commit
782d94f6c0
10
.eslintrc.js
10
.eslintrc.js
|
@ -126,7 +126,7 @@ const appConfig = {
|
|||
ignoreParameters: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
'@typescript-eslint/no-this-alias': 'error',
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
|
@ -139,7 +139,6 @@ const appConfig = {
|
|||
'always',
|
||||
],
|
||||
'@typescript-eslint/type-annotation-spacing': 'error',
|
||||
'@typescript-eslint/unified-signatures': 'error',
|
||||
'header/header': [
|
||||
2,
|
||||
'line',
|
||||
|
@ -235,6 +234,11 @@ const appConfig = {
|
|||
prev: '*',
|
||||
next: 'return',
|
||||
},
|
||||
{
|
||||
blankLine: 'always',
|
||||
prev: '*',
|
||||
next: 'function',
|
||||
},
|
||||
],
|
||||
'prefer-arrow/prefer-arrow-functions': [
|
||||
'error',
|
||||
|
@ -271,6 +275,7 @@ testsConfig['rules']['padded-blocks'] = [
|
|||
switches: 'never',
|
||||
},
|
||||
];
|
||||
testsConfig['rules']['jest/expect-expect'] = 'off';
|
||||
testsConfig['plugins'].push('jest');
|
||||
testsConfig['extends'].push('plugin:jest/recommended');
|
||||
|
||||
|
@ -291,6 +296,7 @@ module.exports = {
|
|||
'@angular-eslint/template/no-positive-tabindex': 'error',
|
||||
'@angular-eslint/template/accessibility-table-scope': 'error',
|
||||
'@angular-eslint/template/accessibility-valid-aria': 'error',
|
||||
'@angular-eslint/template/no-duplicate-attributes': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
* text=auto
|
||||
*.ts eol=lf
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
node-version: '14.x'
|
||||
- run: npm ci
|
||||
- run: result=$(find src -type f -iname '*.html' -exec sh -c 'cat {} | tr "\n" " " | grep -Eo "class=\"[^\"]+\"[^>]+class=\"" ' \; | wc -l); test $result -eq 0
|
||||
- run: npm install -D @ionic/v4-migration-tslint
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
name: Performance
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
performance:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
MOODLE_DOCKER_DB: pgsql
|
||||
MOODLE_DOCKER_BROWSER: chrome
|
||||
MOODLE_DOCKER_PHP_VERSION: 7.3
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- id: nvmrc
|
||||
uses: browniebroke/read-nvmrc-action@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '${{ steps.nvmrc.outputs.node_version }}'
|
||||
- name: Additional checkouts
|
||||
run: |
|
||||
git clone --branch master --depth 1 https://github.com/moodle/moodle $GITHUB_WORKSPACE/moodle
|
||||
git clone --branch integration --depth 1 https://github.com/moodlehq/moodle-local_moodlemobileapp $GITHUB_WORKSPACE/moodle/local/moodlemobileapp
|
||||
git clone --branch master --depth 1 https://github.com/moodlehq/moodle-docker $GITHUB_WORKSPACE/moodle-docker
|
||||
- name: Install npm packages
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
npm ci --no-audit
|
||||
- name: Generate Behat tests plugin
|
||||
run: |
|
||||
export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
|
||||
npx gulp behat
|
||||
- name: Configure & launch Moodle with Docker
|
||||
run: |
|
||||
export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
|
||||
cp $GITHUB_WORKSPACE/moodle-docker/config.docker-template.php $GITHUB_WORKSPACE/moodle/config.php
|
||||
sed -i "59i\ 'capabilities' => [" $GITHUB_WORKSPACE/moodle/config.php
|
||||
sed -i "60i\ 'extra_capabilities' => [" $GITHUB_WORKSPACE/moodle/config.php
|
||||
sed -i "61i\ 'goog:loggingPrefs' => ['performance' => 'ALL']," $GITHUB_WORKSPACE/moodle/config.php
|
||||
sed -i "62i\ 'chromeOptions' => ['perfLoggingPrefs' => ['traceCategories' => 'devtools.timeline']]," $GITHUB_WORKSPACE/moodle/config.php
|
||||
sed -i "63i\ ]," $GITHUB_WORKSPACE/moodle/config.php
|
||||
sed -i "64i\ ]," $GITHUB_WORKSPACE/moodle/config.php
|
||||
sed -i "76i\$CFG->behat_ionic_wwwroot = 'http://moodleapp';" $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: Compile & launch production app with Docker
|
||||
run: |
|
||||
docker build -t moodlehq/moodleapp:performance .
|
||||
docker run -d --rm --name moodleapp moodlehq/moodleapp:performance
|
||||
docker network connect moodle-docker_default moodleapp --alias moodleapp
|
||||
- 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"
|
||||
- name: Run performance tests
|
||||
run: |
|
||||
export MOODLE_DOCKER_WWWROOT=$GITHUB_WORKSPACE/moodle
|
||||
for i in {0..2}
|
||||
do
|
||||
$GITHUB_WORKSPACE/moodle-docker/bin/moodle-docker-compose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags="@performance" --auto-rerun"
|
||||
done
|
||||
- name: Show performance results
|
||||
run: node ./scripts/print-performance-measures.js $GITHUB_WORKSPACE/moodle/behatperformancemeasures/
|
|
@ -12,9 +12,11 @@ jobs:
|
|||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
node-version: '14'
|
||||
- name: Install npm packages
|
||||
run: npm ci
|
||||
run: |
|
||||
npm install -g npm@7
|
||||
npm ci --no-audit
|
||||
- name: Check langindex
|
||||
run: |
|
||||
result=$(cat scripts/langindex.json | grep \"TBD\" | wc -l); test $result -eq 0
|
||||
|
@ -49,11 +51,11 @@ jobs:
|
|||
echo "Found $found missing langkeys"
|
||||
exit 1
|
||||
fi
|
||||
- name: Run Linter
|
||||
run: npm run lint
|
||||
- name: Run Linter (ignore warnings)
|
||||
run: npm run lint -- --quiet
|
||||
- name: Run tests
|
||||
run: npm run test:ci
|
||||
- name: Production builds
|
||||
run: npm run build:prod
|
||||
- name: JavaScript code compatibility
|
||||
run: result=$(npx check-es-compat www/*.js 2> /dev/null | grep -v -E "Array\.prototype\.includes|Promise\.prototype\.finally|String\.prototype\.(matchAll|trimRight)|globalThis" | grep -Po "(?<=error).*?(?=\s+ecmascript)" | wc -l); test $result -eq 0
|
||||
run: result=$(npx check-es-compat www/*.js 2> /dev/null | grep -v -E "Array\.prototype\.includes|Promise\.prototype\.finally|String\.prototype\.(matchAll|trimRight)|globalThis" | grep -Po "(?<=error).*?(?=\s+ecmascript)" | wc -l); test $result -eq 1
|
||||
|
|
22
.travis.yml
22
.travis.yml
|
@ -1,6 +1,6 @@
|
|||
os: linux
|
||||
dist: trusty
|
||||
node_js: 12
|
||||
node_js: 14
|
||||
|
||||
git:
|
||||
depth: 3
|
||||
|
@ -18,12 +18,12 @@ cache:
|
|||
- $HOME/.android/build-cache
|
||||
|
||||
before_install:
|
||||
- nvm install 12
|
||||
- nvm install
|
||||
- npm install npm@^7 -g
|
||||
- node --version
|
||||
- npm --version
|
||||
- nvm --version
|
||||
- npm ci
|
||||
- npm install npm@^6 -g
|
||||
|
||||
before_script:
|
||||
- npx gulp
|
||||
|
@ -46,16 +46,30 @@ jobs:
|
|||
- extra-google-google_play_services
|
||||
- extra-google-m2repository
|
||||
- extra-android-m2repository
|
||||
before_install:
|
||||
- nvm install
|
||||
- npm install npm@^7 -g
|
||||
- node --version
|
||||
- npm --version
|
||||
- nvm --version
|
||||
- npm ci
|
||||
- yes | sdkmanager "build-tools;30.0.3"
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libsecret-1-dev
|
||||
- php5-cli
|
||||
- php5-common
|
||||
- stage: build
|
||||
name: "Build iOS"
|
||||
language: node_js
|
||||
if: env(BUILD_IOS) = 1 AND (env(DEPLOY) = 1 OR (env(DEPLOY) = 2 AND tag IS NOT blank))
|
||||
os: osx
|
||||
osx_image: xcode12.5
|
||||
osx_image: xcode13.1
|
||||
addons:
|
||||
homebrew:
|
||||
packages:
|
||||
- jq
|
||||
- stage: test
|
||||
name: "End to end tests (mod_forum and mod_messages)"
|
||||
services:
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
|
@ -1,5 +1,24 @@
|
|||
{
|
||||
|
||||
/**
|
||||
* Formatting.
|
||||
*/
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features",
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.format.enable": true,
|
||||
"html.format.endWithNewline": true,
|
||||
"html.format.wrapLineLength": 140,
|
||||
"files.eol": "\n",
|
||||
"files.trimFinalNewlines": true,
|
||||
"files.insertFinalNewline": true,
|
||||
"files.trimTrailingWhitespace": true,
|
||||
|
||||
/**
|
||||
* Config files.
|
||||
*/
|
||||
"files.associations": {
|
||||
"moodle.config.json": "jsonc",
|
||||
"moodle.config.*.json": "jsonc",
|
||||
|
|
|
@ -6,7 +6,8 @@ WORKDIR /app
|
|||
# Prepare node dependencies
|
||||
RUN apt-get update && apt-get install libsecret-1-0 -y
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
RUN npm install -g npm@7
|
||||
RUN npm ci --no-audit
|
||||
|
||||
# Build source
|
||||
ARG build_command="npm run build:prod"
|
||||
|
|
19
README.md
19
README.md
|
@ -1,22 +1,15 @@
|
|||
Moodle Mobile
|
||||
Moodle App
|
||||
=================
|
||||
|
||||
This is the primary repository of source code for the official Moodle Mobile app.
|
||||
This is the primary repository of source code for the official mobile app for Moodle.
|
||||
|
||||
* [User documentation](http://docs.moodle.org/en/Moodle_Mobile)
|
||||
* [Developer documentation](http://docs.moodle.org/dev/Moodle_Mobile)
|
||||
* [Development environment setup](http://docs.moodle.org/dev/Setting_up_your_development_environment_for_Moodle_Mobile_2)
|
||||
* [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)
|
||||
* [Bug Tracker](https://tracker.moodle.org/browse/MOBILE)
|
||||
* [Release Notes](http://docs.moodle.org/dev/Moodle_Mobile_Release_Notes)
|
||||
* [Release Notes](https://docs.moodle.org/dev/Moodle_App_Release_Notes)
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
[Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Big Thanks
|
||||
-----------
|
||||
|
||||
Cross-browser Testing Platform and Open Source <3 Provided by [Sauce Labs](https://saucelabs.com)
|
||||
|
||||
![Sauce Labs Logo](https://user-images.githubusercontent.com/557037/43443976-d88d5a78-94a2-11e8-8915-9f06521423dd.png)
|
26
angular.json
26
angular.json
|
@ -12,8 +12,14 @@
|
|||
"schematics": {},
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"customWebpackConfig": {
|
||||
"path": "./webpack.config.js"
|
||||
},
|
||||
"allowedCommonJsDependencies":[
|
||||
"chart.js"
|
||||
],
|
||||
"outputPath": "www",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
|
@ -55,11 +61,25 @@
|
|||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "50mb",
|
||||
"maximumError": "100mb"
|
||||
"maximumWarning": "5mb",
|
||||
"maximumError": "20mb"
|
||||
}
|
||||
]
|
||||
},
|
||||
"testing": {
|
||||
"optimization": {
|
||||
"scripts": false,
|
||||
"styles": true
|
||||
},
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true
|
||||
},
|
||||
"ci": {
|
||||
"progress": false
|
||||
}
|
||||
|
|
28
config.xml
28
config.xml
|
@ -1,5 +1,5 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<widget android-versionCode="39503" id="com.moodle.moodlemobile" ios-CFBundleVersion="3.9.5.3" version="3.9.5" versionCode="39503" 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="40001" id="com.moodle.moodlemobile" ios-CFBundleVersion="4.0.0.1" version="4.0.0" versionCode="40001" 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" />
|
||||
<preference name="AppendUserAgent" value="MoodleMobile 4.0.0 (40000)" />
|
||||
<preference name="BackupWebStorage" value="none" />
|
||||
<preference name="ScrollEnabled" value="false" />
|
||||
<preference name="KeyboardDisplayRequiresUserAction" value="false" />
|
||||
|
@ -47,6 +47,11 @@
|
|||
<preference name="iosPersistentFileLocation" value="Compatibility" />
|
||||
<preference name="iosScheme" value="moodleappfs" />
|
||||
<preference name="WKWebViewOnly" value="true" />
|
||||
<preference name="WKFullScreenEnabled" value="true" />
|
||||
<preference name="AndroidXEnabled" value="true" />
|
||||
<preference name="GradlePluginGoogleServicesEnabled" value="true" />
|
||||
<preference name="GradlePluginGoogleServicesVersion" value="4.3.10" />
|
||||
<preference name="StatusBarOverlaysWebView" value="false" />
|
||||
<feature name="StatusBar">
|
||||
<param name="ios-package" onload="true" value="CDVStatusBar" />
|
||||
</feature>
|
||||
|
@ -57,11 +62,12 @@
|
|||
<resource-file src="resources/android/icon/drawable-mdpi-smallicon.png" target="app/src/main/res/mipmap-mdpi/smallicon.png" />
|
||||
<resource-file src="resources/android/icon/drawable-hdpi-smallicon.png" target="app/src/main/res/mipmap-hdpi/smallicon.png" />
|
||||
<resource-file src="resources/android/icon/drawable-xhdpi-smallicon.png" target="app/src/main/res/mipmap-xhdpi/smallicon.png" />
|
||||
<resource-file src="resources/android/xml/network_security_config.xml" target="app/src/main/res/xml/network_security_config.xml" />
|
||||
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application/activity[@android:name='MainActivity']">
|
||||
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" />
|
||||
</edit-config>
|
||||
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application">
|
||||
<application android:largeHeap="true" android:usesCleartextTraffic="true" />
|
||||
<application android:largeHeap="true" android:networkSecurityConfig="@xml/network_security_config" />
|
||||
</edit-config>
|
||||
<config-file parent="/manifest/application" target="AndroidManifest.xml">
|
||||
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
|
||||
|
@ -124,11 +130,6 @@
|
|||
<param name="android-package" value="org.apache.cordova.geolocation.Geolocation" />
|
||||
</feature>
|
||||
</config-file>
|
||||
<config-file parent="/*" target="res/xml/config.xml">
|
||||
<feature name="Globalization">
|
||||
<param name="android-package" value="org.apache.cordova.globalization.Globalization" />
|
||||
</feature>
|
||||
</config-file>
|
||||
<config-file parent="/*" target="res/xml/config.xml">
|
||||
<feature name="InAppBrowser">
|
||||
<param name="android-package" value="org.apache.cordova.inappbrowser.InAppBrowser" />
|
||||
|
@ -185,12 +186,6 @@
|
|||
<param name="onload" value="true" />
|
||||
</feature>
|
||||
</config-file>
|
||||
<config-file parent="/*" target="res/xml/config.xml">
|
||||
<feature name="Whitelist">
|
||||
<param name="android-package" value="org.apache.cordova.whitelist.WhitelistPlugin" />
|
||||
<param name="onload" value="true" />
|
||||
</feature>
|
||||
</config-file>
|
||||
<config-file parent="/*" target="res/xml/config.xml">
|
||||
<feature name="SQLitePlugin">
|
||||
<param name="android-package" value="io.sqlc.SQLitePlugin" />
|
||||
|
@ -256,7 +251,7 @@
|
|||
<true />
|
||||
</edit-config>
|
||||
<edit-config file="*-Info.plist" mode="merge" target="CFBundleShortVersionString">
|
||||
<string>3.9.5</string>
|
||||
<string>4.0.0</string>
|
||||
</edit-config>
|
||||
<edit-config file="*-Info.plist" mode="overwrite" target="CFBundleLocalizations">
|
||||
<array>
|
||||
|
@ -288,6 +283,9 @@
|
|||
<config-file parent="NSCrossWebsiteTrackingUsageDescription" target="*-Info.plist">
|
||||
<string>This app needs third party cookies to correctly render embedded content from the Moodle site.</string>
|
||||
</config-file>
|
||||
<config-file parent="ITSAppUsesNonExemptEncryption" target="*-Info.plist">
|
||||
<false />
|
||||
</config-file>
|
||||
<config-file parent="CFBundleDocumentTypes" target="*-Info.plist">
|
||||
<array>
|
||||
<dict>
|
||||
|
|
|
@ -69,3 +69,7 @@ gulp.task('watch', () => {
|
|||
gulp.watch(['./tests/behat'], { interval: 500 }, gulp.parallel('behat'));
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('watch-behat', () => {
|
||||
gulp.watch(['./tests/behat'], { interval: 500 }, gulp.parallel('behat'));
|
||||
});
|
||||
|
|
2226
licenses.json
2226
licenses.json
File diff suppressed because it is too large
Load Diff
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"app_id": "com.moodle.moodlemobile",
|
||||
"appname": "Moodle Mobile",
|
||||
"versioncode": 3950,
|
||||
"versionname": "3.9.5",
|
||||
"versioncode": 40000,
|
||||
"versionname": "4.0.0",
|
||||
"cache_update_frequency_usually": 420000,
|
||||
"cache_update_frequency_often": 1200000,
|
||||
"cache_update_frequency_sometimes": 3600000,
|
||||
|
@ -30,6 +30,7 @@
|
|||
"he": "עברית",
|
||||
"hi": "हिंदी",
|
||||
"hr": "Hrvatski",
|
||||
"hsb": "Hornjoserbsski",
|
||||
"hu": "magyar",
|
||||
"hy": "Հայերեն",
|
||||
"id": "Indonesian",
|
||||
|
@ -38,6 +39,7 @@
|
|||
"km": "ខ្មែរ",
|
||||
"kn": "ಕನ್ನಡ",
|
||||
"ko": "한국어",
|
||||
"lo": "ລາວ",
|
||||
"lt": "Lietuvių",
|
||||
"lv": "Latviešu",
|
||||
"mn": "Монгол",
|
||||
|
@ -62,15 +64,14 @@
|
|||
"zh-tw": "正體中文"
|
||||
},
|
||||
"wsservice": "moodle_mobile_app",
|
||||
"wsextservice": "local_mobile",
|
||||
"demo_sites": {
|
||||
"student": {
|
||||
"url": "https:\/\/school.moodledemo.net",
|
||||
"url": "https://school.moodledemo.net",
|
||||
"username": "student",
|
||||
"password": "moodle"
|
||||
},
|
||||
"teacher": {
|
||||
"url": "https:\/\/school.moodledemo.net",
|
||||
"url": "https://school.moodledemo.net",
|
||||
"username": "teacher",
|
||||
"password": "moodle"
|
||||
}
|
||||
|
@ -88,7 +89,7 @@
|
|||
"onlyallowlistedsites": false,
|
||||
"skipssoconfirmation": false,
|
||||
"forcedefaultlanguage": false,
|
||||
"privacypolicy": "https:\/\/moodle.net\/moodle-app-privacy\/",
|
||||
"privacypolicy": "https://moodle.net/moodle-app-privacy/",
|
||||
"notificoncolor": "#f98012",
|
||||
"enableanalytics": false,
|
||||
"enableonboarding": true,
|
||||
|
@ -98,5 +99,8 @@
|
|||
"appstores": {
|
||||
"android": "com.moodle.moodlemobile",
|
||||
"ios": "id633359593"
|
||||
}
|
||||
},
|
||||
"wsrequestqueuelimit": 10,
|
||||
"wsrequestqueuedelay": 100,
|
||||
"calendarreminderdefaultvalue": 3600
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
124
package.json
124
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "moodlemobile",
|
||||
"version": "3.9.5",
|
||||
"version": "4.0.0",
|
||||
"description": "The official app for Moodle.",
|
||||
"author": {
|
||||
"name": "Moodle Pty Ltd.",
|
||||
|
@ -8,7 +8,7 @@
|
|||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/moodlehq/moodlemobile2.git"
|
||||
"url": "https://github.com/moodlehq/moodleapp.git"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"licenses": [
|
||||
|
@ -19,21 +19,22 @@
|
|||
],
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ionic serve",
|
||||
"start": "ionic serve --browser=$MOODLE_APP_BROWSER",
|
||||
"serve:test": "NODE_ENV=testing ionic serve --no-open",
|
||||
"build": "ionic build",
|
||||
"build:prod": "NODE_ENV=production ionic build --prod",
|
||||
"build:test": "NODE_ENV=testing ionic build",
|
||||
"build:test": "NODE_ENV=testing ionic build --configuration=testing",
|
||||
"dev:android": "ionic cordova run android --livereload",
|
||||
"dev:ios": "ionic cordova run ios --livereload",
|
||||
"prod:android": "NODE_ENV=production ionic cordova run android --aot",
|
||||
"prod:ios": "NODE_ENV=production ionic cordova run ios --aot",
|
||||
"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",
|
||||
"test": "NODE_ENV=testing gulp && jest --verbose",
|
||||
"test:ci": "NODE_ENV=testing gulp && jest -ci --runInBand --verbose",
|
||||
"test:watch": "NODE_ENV=testing gulp watch & jest --watch",
|
||||
"test:coverage": "NODE_ENV=testing gulp && jest --coverage",
|
||||
"lint": "NODE_OPTIONS=--max-old-space-size=4096 ng lint",
|
||||
"ionic:serve:before": "gulp",
|
||||
"ionic:serve": "gulp watch & NODE_OPTIONS=--max-old-space-size=4096 ng serve",
|
||||
"ionic:serve": "cross-env-shell ./scripts/serve.sh",
|
||||
"ionic:build:before": "gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -70,65 +71,61 @@
|
|||
"@ionic-native/status-bar": "5.33.0",
|
||||
"@ionic-native/web-intent": "5.33.0",
|
||||
"@ionic-native/zip": "5.33.0",
|
||||
"@ionic/angular": "5.6.6",
|
||||
"@ionic/angular": "5.9.2",
|
||||
"@moodlehq/cordova-plugin-file-transfer": "1.7.1-moodle.5",
|
||||
"@moodlehq/cordova-plugin-inappbrowser": "5.0.0-moodle.3",
|
||||
"@moodlehq/cordova-plugin-ionic-webview": "5.0.0-moodle.1",
|
||||
"@moodlehq/cordova-plugin-local-notification": "0.9.0-moodle.3",
|
||||
"@moodlehq/cordova-plugin-qrscanner": "3.0.1-moodle.2",
|
||||
"@moodlehq/cordova-plugin-zip": "3.1.0-moodle.1",
|
||||
"@moodlehq/phonegap-plugin-push": "2.0.0-moodle.4",
|
||||
"@ngx-translate/core": "13.0.0",
|
||||
"@ngx-translate/http-loader": "6.0.0",
|
||||
"@types/chart.js": "2.9.31",
|
||||
"@types/cordova": "0.0.34",
|
||||
"@types/cordova-plugin-file-transfer": "1.6.2",
|
||||
"@types/dom-mediacapture-record": "1.0.7",
|
||||
"chart.js": "2.9.4",
|
||||
"com-darryncampbell-cordova-plugin-intent": "1.3.0",
|
||||
"cordova": "10.0.0",
|
||||
"cordova-android": "9.1.0",
|
||||
"cordova-android-support-gradle-release": "3.0.1",
|
||||
"com-darryncampbell-cordova-plugin-intent": "2.2.0",
|
||||
"cordova": "11.0.0",
|
||||
"cordova-android": "10.1.1",
|
||||
"cordova-clipboard": "1.3.0",
|
||||
"cordova-ios": "6.2.0",
|
||||
"cordova-plugin-add-swift-support": "2.0.2",
|
||||
"cordova-plugin-advanced-http": "3.1.0",
|
||||
"cordova-plugin-advanced-http": "3.2.2",
|
||||
"cordova-plugin-badge": "0.8.8",
|
||||
"cordova-plugin-camera": "5.0.1",
|
||||
"cordova-plugin-camera": "6.0.0",
|
||||
"cordova-plugin-chooser": "1.3.2",
|
||||
"cordova-plugin-customurlscheme": "5.0.2",
|
||||
"cordova-plugin-device": "2.0.3",
|
||||
"cordova-plugin-file": "6.0.2",
|
||||
"cordova-plugin-file-opener2": "3.0.5",
|
||||
"cordova-plugin-file-transfer": "git+https://github.com/moodlemobile/cordova-plugin-file-transfer.git",
|
||||
"cordova-plugin-geolocation": "4.1.0",
|
||||
"cordova-plugin-globalization": "1.11.0",
|
||||
"cordova-plugin-inappbrowser": "git+https://github.com/moodlemobile/cordova-plugin-inappbrowser.git#moodle-ionic5",
|
||||
"cordova-plugin-ionic-keyboard": "2.2.0",
|
||||
"cordova-plugin-ionic-webview": "5.0.0",
|
||||
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
|
||||
"cordova-plugin-media": "5.0.3",
|
||||
"cordova-plugin-media": "5.0.4",
|
||||
"cordova-plugin-media-capture": "3.0.3",
|
||||
"cordova-plugin-network-information": "2.0.2",
|
||||
"cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist",
|
||||
"cordova-plugin-screen-orientation": "3.0.2",
|
||||
"cordova-plugin-network-information": "3.0.0",
|
||||
"cordova-plugin-prevent-override": "1.0.1",
|
||||
"cordova-plugin-splashscreen": "6.0.0",
|
||||
"cordova-plugin-statusbar": "2.4.3",
|
||||
"cordova-plugin-whitelist": "1.3.4",
|
||||
"cordova-plugin-wkuserscript": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git",
|
||||
"cordova-plugin-wkwebview-cookies": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git",
|
||||
"cordova-plugin-zip": "3.1.0",
|
||||
"cordova-plugin-statusbar": "3.0.0",
|
||||
"cordova-plugin-wkuserscript": "1.0.1",
|
||||
"cordova-plugin-wkwebview-cookies": "1.0.1",
|
||||
"cordova-sqlite-storage": "6.0.0",
|
||||
"cordova-support-google-services": "1.3.2",
|
||||
"cordova.plugins.diagnostic": "5.0.2",
|
||||
"cordova.plugins.diagnostic": "6.1.1",
|
||||
"core-js": "3.9.1",
|
||||
"es6-promise-plugin": "4.2.2",
|
||||
"jszip": "3.5.0",
|
||||
"hammerjs": "2.0.8",
|
||||
"jszip": "3.7.1",
|
||||
"mathjax": "2.7.7",
|
||||
"moment": "2.29.0",
|
||||
"moment": "2.29.2",
|
||||
"nl.kingsquare.cordova.background-audio": "1.0.1",
|
||||
"phonegap-plugin-multidex": "1.0.0",
|
||||
"phonegap-plugin-push": "git+https://github.com/moodlemobile/phonegap-plugin-push.git#moodle-v3",
|
||||
"rxjs": "6.5.5",
|
||||
"ts-md5": "1.2.7",
|
||||
"tslib": "2.0.1",
|
||||
"tslib": "2.3.1",
|
||||
"zone.js": "0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/architect": "0.1101.2",
|
||||
"@angular-builders/custom-webpack": "10.0.1",
|
||||
"@angular-devkit/architect": "0.1202.7",
|
||||
"@angular-devkit/build-angular": "0.1000.8",
|
||||
"@angular-eslint/builder": "4.2.0",
|
||||
"@angular-eslint/eslint-plugin": "4.2.0",
|
||||
|
@ -140,7 +137,7 @@
|
|||
"@angular/compiler-cli": "10.0.14",
|
||||
"@angular/language-service": "10.0.14",
|
||||
"@ionic/angular-toolkit": "2.3.3",
|
||||
"@ionic/cli": "6.14.1",
|
||||
"@ionic/cli": "6.19.0",
|
||||
"@types/faker": "5.1.3",
|
||||
"@types/node": "12.12.64",
|
||||
"@types/resize-observer-browser": "0.1.5",
|
||||
|
@ -148,7 +145,9 @@
|
|||
"@typescript-eslint/eslint-plugin": "4.22.0",
|
||||
"@typescript-eslint/parser": "4.22.0",
|
||||
"check-es-compat": "1.1.1",
|
||||
"cordova-plugin-prevent-override": "git+https://github.com/moodlemobile/cordova-plugin-prevent-override.git",
|
||||
"cordova-plugin-androidx-adapter": "1.1.3",
|
||||
"cordova-plugin-screen-orientation": "^3.0.2",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "7.25.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
|
@ -169,13 +168,14 @@
|
|||
"jest": "26.5.2",
|
||||
"jest-preset-angular": "8.3.1",
|
||||
"jsonc-parser": "2.3.1",
|
||||
"native-run": "^1.4.0",
|
||||
"native-run": "1.4.0",
|
||||
"terser-webpack-plugin": "4.2.3",
|
||||
"ts-jest": "26.4.1",
|
||||
"ts-node": "8.3.0",
|
||||
"typescript": "3.9.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.x"
|
||||
"node": ">=14.15.0 <15"
|
||||
},
|
||||
"cordova": {
|
||||
"platforms": [
|
||||
|
@ -183,11 +183,14 @@
|
|||
"ios"
|
||||
],
|
||||
"plugins": {
|
||||
"cordova-plugin-advanced-http": {},
|
||||
"cordova-plugin-advanced-http": {
|
||||
"ANDROIDBLACKLISTSECURESOCKETPROTOCOLS": "SSLv3,TLSv1"
|
||||
},
|
||||
"cordova-clipboard": {},
|
||||
"cordova-plugin-badge": {},
|
||||
"cordova-plugin-camera": {
|
||||
"ANDROID_SUPPORT_V4_VERSION": "27.+"
|
||||
"ANDROID_SUPPORT_V4_VERSION": "27.+",
|
||||
"ANDROIDX_CORE_VERSION": "1.6.+"
|
||||
},
|
||||
"cordova-plugin-chooser": {},
|
||||
"cordova-plugin-customurlscheme": {
|
||||
|
@ -203,10 +206,10 @@
|
|||
"cordova-plugin-geolocation": {
|
||||
"GPS_REQUIRED": "false"
|
||||
},
|
||||
"cordova-plugin-inappbrowser": {},
|
||||
"@moodlehq/cordova-plugin-inappbrowser": {},
|
||||
"cordova-plugin-ionic-keyboard": {},
|
||||
"cordova-plugin-ionic-webview": {},
|
||||
"cordova-plugin-local-notification": {
|
||||
"@moodlehq/cordova-plugin-ionic-webview": {},
|
||||
"@moodlehq/cordova-plugin-local-notification": {
|
||||
"ANDROID_SUPPORT_V4_VERSION": "26.+"
|
||||
},
|
||||
"cordova-plugin-media-capture": {},
|
||||
|
@ -214,30 +217,29 @@
|
|||
"KEEP_AVAUDIOSESSION_ALWAYS_ACTIVE": "NO"
|
||||
},
|
||||
"cordova-plugin-network-information": {},
|
||||
"cordova-plugin-qrscanner": {},
|
||||
"cordova-plugin-screen-orientation": {},
|
||||
"@moodlehq/cordova-plugin-qrscanner": {},
|
||||
"cordova-plugin-splashscreen": {},
|
||||
"cordova-plugin-statusbar": {},
|
||||
"cordova-plugin-whitelist": {},
|
||||
"cordova-plugin-wkuserscript": {},
|
||||
"cordova-plugin-wkwebview-cookies": {},
|
||||
"cordova-plugin-zip": {},
|
||||
"@moodlehq/cordova-plugin-zip": {},
|
||||
"cordova-sqlite-storage": {},
|
||||
"phonegap-plugin-push": {
|
||||
"ANDROID_SUPPORT_V13_VERSION": "27.+",
|
||||
"FCM_VERSION": "17.0.+"
|
||||
"@moodlehq/phonegap-plugin-push": {
|
||||
"ANDROID_SUPPORT_V13_VERSION": "28.0.0",
|
||||
"FCM_VERSION": "18.+",
|
||||
"IOS_FIREBASE_MESSAGING_VERSION": "~> 6.32.2"
|
||||
},
|
||||
"com-darryncampbell-cordova-plugin-intent": {},
|
||||
"nl.kingsquare.cordova.background-audio": {},
|
||||
"cordova-android-support-gradle-release": {
|
||||
"ANDROID_SUPPORT_VERSION": "27.+"
|
||||
},
|
||||
"cordova.plugins.diagnostic": {
|
||||
"ANDROID_SUPPORT_VERSION": "28.+"
|
||||
"ANDROID_SUPPORT_VERSION": "28.+",
|
||||
"ANDROIDX_VERSION": "1.0.0",
|
||||
"ANDROIDX_APPCOMPAT_VERSION": "1.3.1"
|
||||
},
|
||||
"cordova-plugin-globalization": {},
|
||||
"cordova-plugin-file-transfer": {},
|
||||
"cordova-plugin-prevent-override": {}
|
||||
"@moodlehq/cordova-plugin-file-transfer": {},
|
||||
"cordova-plugin-prevent-override": {},
|
||||
"cordova-plugin-androidx-adapter": {},
|
||||
"cordova-plugin-screen-orientation": {}
|
||||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true" />
|
||||
</network-security-config>
|
|
@ -113,10 +113,17 @@ function add_langs_to_config($langs, $config) {
|
|||
$config['languages'] = json_decode( json_encode( $config['languages'] ), true );
|
||||
ksort($config['languages']);
|
||||
$config['languages'] = json_decode( json_encode( $config['languages'] ), false );
|
||||
file_put_contents(CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
save_json(CONFIG, $config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save json data.
|
||||
*/
|
||||
function save_json($path, $content) {
|
||||
file_put_contents($path, str_replace('\/', '/', json_encode($content, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT))."\n");
|
||||
}
|
||||
|
||||
function get_langfolder($lang) {
|
||||
$folder = LANGPACKSFOLDER.'/'.str_replace('-', '_', $lang);
|
||||
if (!is_dir($folder) || !is_file($folder.'/langconfig.php')) {
|
||||
|
@ -246,9 +253,11 @@ function build_lang($lang, $keys) {
|
|||
}
|
||||
|
||||
if ($value->file != 'local_moodlemobileapp') {
|
||||
$text = str_replace('$a->@', '$a.', $text);
|
||||
$text = str_replace('$a->', '$a.', $text);
|
||||
$text = str_replace('{$a', '{{$a', $text);
|
||||
$text = str_replace('}', '}}', $text);
|
||||
$text = preg_replace('/@@.+?@@(<br>)?\\s*/', '', $text);
|
||||
// Prevent double.
|
||||
$text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text);
|
||||
} else {
|
||||
|
@ -270,7 +279,7 @@ function build_lang($lang, $keys) {
|
|||
|
||||
// Sort and save.
|
||||
ksort($translations);
|
||||
file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
|
||||
save_json(ASSETSPATH.$lang.'.json', $translations);
|
||||
|
||||
$success = count($translations);
|
||||
$percentage = floor($success/$total * 100);
|
||||
|
@ -365,7 +374,7 @@ function save_key($key, $value, $filePath) {
|
|||
if (!isset($file[$key]) || $file[$key] != $value) {
|
||||
$file[$key] = $value;
|
||||
ksort($file);
|
||||
file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
|
||||
save_json($filePath, $file);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,15 @@ function get_language {
|
|||
pushd $LANGPACKSFOLDER > /dev/null
|
||||
|
||||
curl -s $MOODLEORG_URL/$lastversion/$lang.zip --output $lang.zip > /dev/null
|
||||
size=$(du -k "$lang.zip" | cut -f 1)
|
||||
if [ ! -n $lang.zip ] || [ $size -le 60 ]; then
|
||||
echo "Wrong language name or corrupt file for $lang"
|
||||
rm $lang.zip
|
||||
|
||||
popd > /dev/null
|
||||
return
|
||||
fi
|
||||
|
||||
rm -R $lang > /dev/null 2>&1> /dev/null
|
||||
unzip -o -u $lang.zip > /dev/null
|
||||
|
||||
|
@ -114,6 +123,11 @@ function get_language {
|
|||
|
||||
# Entry function to get all language files.
|
||||
function get_languages {
|
||||
suffix=$1
|
||||
if [ -z $suffix ]; then
|
||||
suffix=''
|
||||
fi
|
||||
|
||||
get_last_version
|
||||
|
||||
if [ -d $LANGPACKSFOLDER ]; then
|
||||
|
@ -131,6 +145,7 @@ function get_languages {
|
|||
|
||||
if [ $AWS_SERVICE -eq 1 ]; then
|
||||
get_all_languages_aws
|
||||
suffix=''
|
||||
else
|
||||
echo "Fallback language list will only get current installation languages"
|
||||
get_installed_languages
|
||||
|
@ -138,5 +153,9 @@ function get_languages {
|
|||
|
||||
for lang in $langs; do
|
||||
get_language "$lang"
|
||||
|
||||
if [ $suffix != '' ]; then
|
||||
get_language "$lang$suffix"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
|
|
@ -40,12 +40,18 @@
|
|||
"addon.block_learningplans.pluginname": "block_lp",
|
||||
"addon.block_myoverview.all": "block_myoverview",
|
||||
"addon.block_myoverview.allincludinghidden": "block_myoverview",
|
||||
"addon.block_myoverview.browseallcourses": "local_moodlemobileapp",
|
||||
"addon.block_myoverview.card": "block_myoverview",
|
||||
"addon.block_myoverview.favourites": "block_myoverview",
|
||||
"addon.block_myoverview.future": "block_myoverview",
|
||||
"addon.block_myoverview.hiddencourses": "block_myoverview",
|
||||
"addon.block_myoverview.inprogress": "block_myoverview",
|
||||
"addon.block_myoverview.lastaccessed": "block_myoverview",
|
||||
"addon.block_myoverview.nocourses": "block_myoverview",
|
||||
"addon.block_myoverview.list": "block_myoverview",
|
||||
"addon.block_myoverview.nocoursesenrolled": "local_moodlemobileapp",
|
||||
"addon.block_myoverview.nocoursesenrolleddescription": "local_moodlemobileapp",
|
||||
"addon.block_myoverview.noresult": "local_moodlemobileapp",
|
||||
"addon.block_myoverview.noresultdescription": "local_moodlemobileapp",
|
||||
"addon.block_myoverview.past": "block_myoverview",
|
||||
"addon.block_myoverview.pluginname": "block_myoverview",
|
||||
"addon.block_myoverview.shortname": "block_myoverview",
|
||||
|
@ -73,6 +79,7 @@
|
|||
"addon.block_timeline.noevents": "block_timeline",
|
||||
"addon.block_timeline.overdue": "block_timeline",
|
||||
"addon.block_timeline.pluginname": "block_timeline",
|
||||
"addon.block_timeline.searchevents": "block_timeline",
|
||||
"addon.block_timeline.sortbycourses": "block_timeline",
|
||||
"addon.block_timeline.sortbydates": "block_timeline",
|
||||
"addon.blog.blog": "blog",
|
||||
|
@ -140,6 +147,7 @@
|
|||
"addon.calendar.sunday": "calendar",
|
||||
"addon.calendar.thu": "calendar",
|
||||
"addon.calendar.thursday": "calendar",
|
||||
"addon.calendar.timebefore": "local_moodlemobileapp",
|
||||
"addon.calendar.today": "calendar",
|
||||
"addon.calendar.tomorrow": "calendar",
|
||||
"addon.calendar.tue": "calendar",
|
||||
|
@ -153,6 +161,7 @@
|
|||
"addon.calendar.typeopen": "calendar",
|
||||
"addon.calendar.typesite": "calendar",
|
||||
"addon.calendar.typeuser": "calendar",
|
||||
"addon.calendar.units": "qtype_numerical",
|
||||
"addon.calendar.upcomingevents": "calendar",
|
||||
"addon.calendar.userevents": "calendar",
|
||||
"addon.calendar.wed": "calendar",
|
||||
|
@ -229,6 +238,7 @@
|
|||
"addon.coursecompletion.status": "moodle",
|
||||
"addon.coursecompletion.viewcoursereport": "completion",
|
||||
"addon.messageoutput_airnotifier.processorsettingsdesc": "local_moodlemobileapp",
|
||||
"addon.messageoutput_airnotifier.pushdisabledwarning": "local_moodlemobileapp",
|
||||
"addon.messages.acceptandaddcontact": "message",
|
||||
"addon.messages.addcontact": "message",
|
||||
"addon.messages.addcontactconfirm": "message",
|
||||
|
@ -325,14 +335,18 @@
|
|||
"addon.mod_assign.allowsubmissionsfromdatesummary": "assign",
|
||||
"addon.mod_assign.applytoteam": "assign",
|
||||
"addon.mod_assign.assignmentisdue": "assign",
|
||||
"addon.mod_assign.assigntimeleft": "assign",
|
||||
"addon.mod_assign.attemptnumber": "assign",
|
||||
"addon.mod_assign.attemptreopenmethod": "assign",
|
||||
"addon.mod_assign.attemptreopenmethod_manual": "assign",
|
||||
"addon.mod_assign.attemptreopenmethod_untilpass": "assign",
|
||||
"addon.mod_assign.attemptsettings": "assign",
|
||||
"addon.mod_assign.beginassignment": "assign",
|
||||
"addon.mod_assign.caneditsubmission": "assign",
|
||||
"addon.mod_assign.cannoteditduetostatementsubmission": "local_moodlemobileapp",
|
||||
"addon.mod_assign.cannotgradefromapp": "local_moodlemobileapp",
|
||||
"addon.mod_assign.cannotsubmitduetostatementsubmission": "local_moodlemobileapp",
|
||||
"addon.mod_assign.confirmstart": "assign",
|
||||
"addon.mod_assign.confirmsubmission": "assign",
|
||||
"addon.mod_assign.currentattempt": "assign",
|
||||
"addon.mod_assign.currentattemptof": "assign",
|
||||
|
@ -340,7 +354,7 @@
|
|||
"addon.mod_assign.cutoffdate": "assign",
|
||||
"addon.mod_assign.defaultteam": "assign",
|
||||
"addon.mod_assign.duedate": "assign",
|
||||
"addon.mod_assign.duedateno": "assign",
|
||||
"addon.mod_assign.duedateno": "local_moodlemobileapp",
|
||||
"addon.mod_assign.duedatereached": "assign",
|
||||
"addon.mod_assign.editingstatus": "assign",
|
||||
"addon.mod_assign.editsubmission": "assign",
|
||||
|
@ -410,7 +424,10 @@
|
|||
"addon.mod_assign.submitassignment_help": "assign",
|
||||
"addon.mod_assign.submittedearly": "assign",
|
||||
"addon.mod_assign.submittedlate": "assign",
|
||||
"addon.mod_assign.submittedovertime": "assign",
|
||||
"addon.mod_assign.submittedundertime": "assign",
|
||||
"addon.mod_assign.syncblockedusercomponent": "local_moodlemobileapp",
|
||||
"addon.mod_assign.timelimit": "assign",
|
||||
"addon.mod_assign.timemodified": "assign",
|
||||
"addon.mod_assign.timeremaining": "assign",
|
||||
"addon.mod_assign.ungroupedusers": "assign",
|
||||
|
@ -428,6 +445,23 @@
|
|||
"addon.mod_assign_submission_file.pluginname": "assignsubmission_file",
|
||||
"addon.mod_assign_submission_onlinetext.pluginname": "assignsubmission_onlinetext",
|
||||
"addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext",
|
||||
"addon.mod_bigbluebuttonbn.end_session_confirm": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.end_session_confirm_title": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.mod_form_field_closingtime": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.mod_form_field_openingtime": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.userlimitreached": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_conference_action_end": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_conference_action_join": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_error_unable_join_student": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_groups_selection_warning": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_message_conference_in_progress": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_message_conference_room_ready": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_message_moderator": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_message_moderators": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_message_session_started_at": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_message_viewer": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_message_viewers": "bigbluebuttonbn",
|
||||
"addon.mod_bigbluebuttonbn.view_nojoin": "bigbluebuttonbn",
|
||||
"addon.mod_book.errorchapter": "book",
|
||||
"addon.mod_book.modulenameplural": "book",
|
||||
"addon.mod_book.navnexttitle": "book",
|
||||
|
@ -538,6 +572,7 @@
|
|||
"addon.mod_feedback.analysis": "feedback",
|
||||
"addon.mod_feedback.anonymous": "feedback",
|
||||
"addon.mod_feedback.anonymous_entries": "feedback",
|
||||
"addon.mod_feedback.anonymous_user": "feedback",
|
||||
"addon.mod_feedback.average": "feedback",
|
||||
"addon.mod_feedback.captchaofflinewarning": "local_moodlemobileapp",
|
||||
"addon.mod_feedback.complete_the_form": "feedback",
|
||||
|
@ -553,7 +588,6 @@
|
|||
"addon.mod_feedback.minimal": "feedback",
|
||||
"addon.mod_feedback.mode": "feedback",
|
||||
"addon.mod_feedback.modulenameplural": "feedback",
|
||||
"addon.mod_feedback.next_page": "feedback",
|
||||
"addon.mod_feedback.non_anonymous": "feedback",
|
||||
"addon.mod_feedback.non_anonymous_entries": "feedback",
|
||||
"addon.mod_feedback.non_respondents_students": "feedback",
|
||||
|
@ -563,7 +597,6 @@
|
|||
"addon.mod_feedback.overview": "feedback",
|
||||
"addon.mod_feedback.page_after_submit": "feedback",
|
||||
"addon.mod_feedback.preview": "moodle",
|
||||
"addon.mod_feedback.previous_page": "feedback",
|
||||
"addon.mod_feedback.questions": "feedback",
|
||||
"addon.mod_feedback.questionscountdescription": "local_moodlemobileapp",
|
||||
"addon.mod_feedback.response_nr": "feedback",
|
||||
|
@ -625,8 +658,8 @@
|
|||
"addon.mod_forum.posttoforum": "forum",
|
||||
"addon.mod_forum.posttomygroups": "forum",
|
||||
"addon.mod_forum.privatereply": "forum",
|
||||
"addon.mod_forum.qandanotify": "forum",
|
||||
"addon.mod_forum.re": "forum",
|
||||
"addon.mod_forum.refreshdiscussions": "local_moodlemobileapp",
|
||||
"addon.mod_forum.refreshposts": "local_moodlemobileapp",
|
||||
"addon.mod_forum.removefromfavourites": "forum",
|
||||
"addon.mod_forum.reply": "forum",
|
||||
|
@ -681,7 +714,9 @@
|
|||
"addon.mod_h5pactivity.attempt_success_fail": "h5pactivity",
|
||||
"addon.mod_h5pactivity.attempt_success_pass": "h5pactivity",
|
||||
"addon.mod_h5pactivity.attempt_success_unknown": "h5pactivity",
|
||||
"addon.mod_h5pactivity.attempts": "h5pactivity",
|
||||
"addon.mod_h5pactivity.attempts_none": "h5pactivity",
|
||||
"addon.mod_h5pactivity.attempts_report": "h5pactivity",
|
||||
"addon.mod_h5pactivity.completion": "h5pactivity",
|
||||
"addon.mod_h5pactivity.downloadh5pfile": "local_moodlemobileapp",
|
||||
"addon.mod_h5pactivity.duration": "h5pactivity",
|
||||
|
@ -692,12 +727,13 @@
|
|||
"addon.mod_h5pactivity.modulenameplural": "h5pactivity",
|
||||
"addon.mod_h5pactivity.myattempts": "h5pactivity",
|
||||
"addon.mod_h5pactivity.no_compatible_track": "h5pactivity",
|
||||
"addon.mod_h5pactivity.noparticipants": "h5pactivity",
|
||||
"addon.mod_h5pactivity.offlinedisabledwarning": "local_moodlemobileapp",
|
||||
"addon.mod_h5pactivity.outcome": "h5pactivity",
|
||||
"addon.mod_h5pactivity.previewmode": "h5pactivity",
|
||||
"addon.mod_h5pactivity.result_fill-in": "h5pactivity",
|
||||
"addon.mod_h5pactivity.result_other": "h5pactivity",
|
||||
"addon.mod_h5pactivity.review_my_attempts": "h5pactivity",
|
||||
"addon.mod_h5pactivity.review_user_attempts": "h5pactivity",
|
||||
"addon.mod_h5pactivity.score": "h5pactivity",
|
||||
"addon.mod_h5pactivity.score_out_of": "h5pactivity",
|
||||
"addon.mod_h5pactivity.startdate": "h5pactivity",
|
||||
|
@ -855,8 +891,6 @@
|
|||
"addon.mod_quiz.requirepasswordmessage": "quizaccess_password",
|
||||
"addon.mod_quiz.returnattempt": "quiz",
|
||||
"addon.mod_quiz.review": "quiz",
|
||||
"addon.mod_quiz.reviewofattempt": "quiz",
|
||||
"addon.mod_quiz.reviewofpreview": "quiz",
|
||||
"addon.mod_quiz.showall": "quiz",
|
||||
"addon.mod_quiz.showeachpage": "quiz",
|
||||
"addon.mod_quiz.startattempt": "quiz",
|
||||
|
@ -883,6 +917,8 @@
|
|||
"addon.mod_resource.modifieddate": "resource",
|
||||
"addon.mod_resource.modulenameplural": "resource",
|
||||
"addon.mod_resource.openthefile": "local_moodlemobileapp",
|
||||
"addon.mod_resource.resourcestatusoutdated": "local_moodlemobileapp",
|
||||
"addon.mod_resource.resourcestatusoutdatedconfirm": "local_moodlemobileapp",
|
||||
"addon.mod_resource.uploadeddate": "resource",
|
||||
"addon.mod_scorm.asset": "scorm",
|
||||
"addon.mod_scorm.assetlaunched": "scorm",
|
||||
|
@ -1069,12 +1105,30 @@
|
|||
"addon.privatefiles.sitefiles": "moodle",
|
||||
"addon.qtype_essay.maxwordlimitboundary": "qtype_essay",
|
||||
"addon.qtype_essay.minwordlimitboundary": "qtype_essay",
|
||||
"addon.storagemanager.deletecourse": "local_moodlemobileapp",
|
||||
"addon.report_insights.actionsaved": "report_insights",
|
||||
"addon.report_insights.fixedack": "analytics",
|
||||
"addon.report_insights.incorrectlyflagged": "analytics",
|
||||
"addon.report_insights.notapplicable": "analytics",
|
||||
"addon.report_insights.notuseful": "analytics",
|
||||
"addon.report_insights.useful": "analytics",
|
||||
"addon.storagemanager.alldata": "tool_wp",
|
||||
"addon.storagemanager.confirmdeleteallsitedata": "local_moodlemobileapp",
|
||||
"addon.storagemanager.confirmdeletecourses": "local_moodlemobileapp",
|
||||
"addon.storagemanager.confirmdeletedatafrom": "local_moodlemobileapp",
|
||||
"addon.storagemanager.coursedownloads": "local_moodlemobileapp",
|
||||
"addon.storagemanager.courseinfo": "local_moodlemobileapp",
|
||||
"addon.storagemanager.deleteall": "moodle",
|
||||
"addon.storagemanager.deleteallsitedata": "local_moodlemobileapp",
|
||||
"addon.storagemanager.deleteallsitedatainfo": "local_moodlemobileapp",
|
||||
"addon.storagemanager.deletecourses": "local_moodlemobileapp",
|
||||
"addon.storagemanager.deletedata": "local_moodlemobileapp",
|
||||
"addon.storagemanager.deletedatafrom": "local_moodlemobileapp",
|
||||
"addon.storagemanager.info": "local_moodlemobileapp",
|
||||
"addon.storagemanager.managestorage": "local_moodlemobileapp",
|
||||
"addon.storagemanager.storageused": "local_moodlemobileapp",
|
||||
"addon.storagemanager.downloadedcourses": "local_moodlemobileapp",
|
||||
"addon.storagemanager.downloads": "local_moodlemobileapp",
|
||||
"addon.storagemanager.errordeletedownloadeddata": "local_moodlemobileapp",
|
||||
"addon.storagemanager.managedownloads": "local_moodlemobileapp",
|
||||
"addon.storagemanager.totaldownloads": "local_moodlemobileapp",
|
||||
"addon.storagemanager.totalspaceusage": "local_moodlemobileapp",
|
||||
"assets.countries.AD": "countries",
|
||||
"assets.countries.AE": "countries",
|
||||
"assets.countries.AF": "countries",
|
||||
|
@ -1324,9 +1378,23 @@
|
|||
"assets.countries.ZA": "countries",
|
||||
"assets.countries.ZM": "countries",
|
||||
"assets.countries.ZW": "countries",
|
||||
"assets.mimetypes.application/dash_xml": "mimetypes",
|
||||
"assets.mimetypes.application/epub_zip": "mimetypes",
|
||||
"assets.mimetypes.application/json": "mimetypes",
|
||||
"assets.mimetypes.application/msword": "mimetypes",
|
||||
"assets.mimetypes.application/pdf": "mimetypes",
|
||||
"assets.mimetypes.application/vnd.google-apps.audio": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.document": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.drawing": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.file": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.folder": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.form": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.fusiontable": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.presentation": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.script": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.site": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.spreadsheet": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.google-apps.video": "local_moodlemobileapp",
|
||||
"assets.mimetypes.application/vnd.moodle.backup": "mimetypes",
|
||||
"assets.mimetypes.application/vnd.ms-excel": "mimetypes",
|
||||
"assets.mimetypes.application/vnd.ms-excel.sheet.macroEnabled.12": "mimetypes",
|
||||
|
@ -1345,6 +1413,7 @@
|
|||
"assets.mimetypes.application/x-iwork-numbers-sffnumbers": "mimetypes",
|
||||
"assets.mimetypes.application/x-iwork-pages-sffpages": "mimetypes",
|
||||
"assets.mimetypes.application/x-javascript": "mimetypes",
|
||||
"assets.mimetypes.application/x-mpegURL": "mimetypes",
|
||||
"assets.mimetypes.application/x-mspublisher": "mimetypes",
|
||||
"assets.mimetypes.application/x-shockwave-flash": "mimetypes",
|
||||
"assets.mimetypes.application/xhtml_xml": "mimetypes",
|
||||
|
@ -1359,6 +1428,8 @@
|
|||
"assets.mimetypes.group:html_track": "mimetypes",
|
||||
"assets.mimetypes.group:html_video": "mimetypes",
|
||||
"assets.mimetypes.group:image": "mimetypes",
|
||||
"assets.mimetypes.group:media_source": "mimetypes",
|
||||
"assets.mimetypes.group:optimised_image": "mimetypes",
|
||||
"assets.mimetypes.group:presentation": "mimetypes",
|
||||
"assets.mimetypes.group:sourcecode": "mimetypes",
|
||||
"assets.mimetypes.group:spreadsheet": "mimetypes",
|
||||
|
@ -1380,6 +1451,7 @@
|
|||
"core.add": "moodle",
|
||||
"core.agelocationverification": "moodle",
|
||||
"core.ago": "message",
|
||||
"core.ajaxendpointnotfound": "local_moodlemobileapp",
|
||||
"core.all": "moodle",
|
||||
"core.allgroups": "moodle",
|
||||
"core.allparticipants": "moodle",
|
||||
|
@ -1388,12 +1460,18 @@
|
|||
"core.areyousure": "moodle",
|
||||
"core.back": "moodle",
|
||||
"core.block.blocks": "moodle",
|
||||
"core.block.noblocks": "error",
|
||||
"core.block.opendrawerblocks": "moodle",
|
||||
"core.block.tour_navigation_dashboard_content": "tool_usertours",
|
||||
"core.block.tour_navigation_dashboard_title": "tool_usertours",
|
||||
"core.browser": "local_moodlemobileapp",
|
||||
"core.calculating": "local_moodlemobileapp",
|
||||
"core.cancel": "moodle",
|
||||
"core.cannotconnect": "local_moodlemobileapp",
|
||||
"core.cannotconnecttrouble": "local_moodlemobileapp",
|
||||
"core.cannotconnectverify": "local_moodlemobileapp",
|
||||
"core.cannotdownloadfiles": "local_moodlemobileapp",
|
||||
"core.cannotlogoutpageblocks": "local_moodlemobileapp",
|
||||
"core.cannotopeninapp": "local_moodlemobileapp",
|
||||
"core.cannotopeninappdownload": "local_moodlemobileapp",
|
||||
"core.captureaudio": "local_moodlemobileapp",
|
||||
|
@ -1401,6 +1479,7 @@
|
|||
"core.captureimage": "local_moodlemobileapp",
|
||||
"core.capturevideo": "local_moodlemobileapp",
|
||||
"core.category": "moodle",
|
||||
"core.certificaterror": "local_moodlemobileapp",
|
||||
"core.choose": "moodle",
|
||||
"core.choosedots": "moodle",
|
||||
"core.clearsearch": "local_moodlemobileapp",
|
||||
|
@ -1433,8 +1512,6 @@
|
|||
"core.completion-alt-manual-y-override": "completion",
|
||||
"core.confirmcanceledit": "local_moodlemobileapp",
|
||||
"core.confirmdeletefile": "repository",
|
||||
"core.confirmgotabroot": "local_moodlemobileapp",
|
||||
"core.confirmgotabrootdefault": "local_moodlemobileapp",
|
||||
"core.confirmleaveunknownchanges": "local_moodlemobileapp",
|
||||
"core.confirmloss": "local_moodlemobileapp",
|
||||
"core.confirmopeninbrowser": "local_moodlemobileapp",
|
||||
|
@ -1453,10 +1530,8 @@
|
|||
"core.course": "moodle",
|
||||
"core.course.activitydisabled": "local_moodlemobileapp",
|
||||
"core.course.activitynotyetviewableremoteaddon": "local_moodlemobileapp",
|
||||
"core.course.activitynotyetviewablesiteupgradeneeded": "local_moodlemobileapp",
|
||||
"core.course.allsections": "local_moodlemobileapp",
|
||||
"core.course.aria:sectionprogress": "local_moodlemobileapp",
|
||||
"core.course.askadmintosupport": "local_moodlemobileapp",
|
||||
"core.course.availablespace": "local_moodlemobileapp",
|
||||
"core.course.cannotdeletewhiledownloading": "local_moodlemobileapp",
|
||||
"core.course.completion_automatic:done": "course",
|
||||
|
@ -1471,34 +1546,47 @@
|
|||
"core.course.completion_setby:manual:done": "course",
|
||||
"core.course.completion_setby:manual:markdone": "course",
|
||||
"core.course.completionrequirements": "course",
|
||||
"core.course.confirmdeletemodulefiles": "local_moodlemobileapp",
|
||||
"core.course.confirmdeletestoreddata": "local_moodlemobileapp",
|
||||
"core.course.confirmdownload": "local_moodlemobileapp",
|
||||
"core.course.confirmdownloadunknownsize": "local_moodlemobileapp",
|
||||
"core.course.confirmdownloadzerosize": "local_moodlemobileapp",
|
||||
"core.course.confirmlimiteddownload": "local_moodlemobileapp",
|
||||
"core.course.confirmpartialdownloadsize": "local_moodlemobileapp",
|
||||
"core.course.contents": "local_moodlemobileapp",
|
||||
"core.course.couldnotloadsectioncontent": "local_moodlemobileapp",
|
||||
"core.course.couldnotloadsections": "local_moodlemobileapp",
|
||||
"core.course.courseindex": "courseformat",
|
||||
"core.course.coursesummary": "moodle",
|
||||
"core.course.done": "completion",
|
||||
"core.course.downloadcourse": "tool_mobile",
|
||||
"core.course.downloadcoursesprogressdescription": "local_moodlemobileapp",
|
||||
"core.course.downloadsectionprogressdescription": "local_moodlemobileapp",
|
||||
"core.course.enddate": "moodle",
|
||||
"core.course.errordownloadingcourse": "local_moodlemobileapp",
|
||||
"core.course.errordownloadingsection": "local_moodlemobileapp",
|
||||
"core.course.errorgetmodule": "local_moodlemobileapp",
|
||||
"core.course.failed": "completion",
|
||||
"core.course.hiddenfromstudents": "moodle",
|
||||
"core.course.hiddenoncoursepage": "moodle",
|
||||
"core.course.highlighted": "moodle",
|
||||
"core.course.insufficientavailablequota": "local_moodlemobileapp",
|
||||
"core.course.insufficientavailablespace": "local_moodlemobileapp",
|
||||
"core.course.lastaccessedactivity": "local_moodlemobileapp",
|
||||
"core.course.manualcompletionnotsynced": "local_moodlemobileapp",
|
||||
"core.course.modulenotfound": "local_moodlemobileapp",
|
||||
"core.course.nextactivity": "local_moodlemobileapp",
|
||||
"core.course.nextactivitynotfound": "local_moodlemobileapp",
|
||||
"core.course.nocontentavailable": "local_moodlemobileapp",
|
||||
"core.course.overriddennotice": "grades",
|
||||
"core.course.previousactivity": "local_moodlemobileapp",
|
||||
"core.course.previousactivitynotfound": "local_moodlemobileapp",
|
||||
"core.course.refreshcourse": "local_moodlemobileapp",
|
||||
"core.course.section": "moodle",
|
||||
"core.course.sections": "moodle",
|
||||
"core.course.startdate": "moodle",
|
||||
"core.course.thisweek": "format_weeks/currentsection",
|
||||
"core.course.todo": "completion",
|
||||
"core.course.tour_navigation_course_index_student_content": "tool_usertours",
|
||||
"core.course.tour_navigation_course_index_student_title": "tool_usertours",
|
||||
"core.course.useactivityonbrowser": "local_moodlemobileapp",
|
||||
"core.course.viewcourse": "block_timeline",
|
||||
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
|
||||
"core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp",
|
||||
"core.coursedetails": "moodle",
|
||||
|
@ -1510,8 +1598,10 @@
|
|||
"core.courses.aria:courseprogress": "block_myoverview",
|
||||
"core.courses.aria:favourite": "course",
|
||||
"core.courses.availablecourses": "moodle",
|
||||
"core.courses.browserenrolinstructions": "local_moodlemobileapp",
|
||||
"core.courses.cannotretrievemorecategories": "local_moodlemobileapp",
|
||||
"core.courses.categories": "moodle",
|
||||
"core.courses.completeenrolmentbrowser": "local_moodlemobileapp",
|
||||
"core.courses.confirmselfenrol": "local_moodlemobileapp",
|
||||
"core.courses.courses": "moodle",
|
||||
"core.courses.downloadcourses": "local_moodlemobileapp",
|
||||
|
@ -1533,22 +1623,25 @@
|
|||
"core.courses.nosearchresults": "wiki",
|
||||
"core.courses.notenroled": "completion",
|
||||
"core.courses.notenrollable": "local_moodlemobileapp",
|
||||
"core.courses.otherenrolments": "local_moodlemobileapp",
|
||||
"core.courses.password": "local_moodlemobileapp",
|
||||
"core.courses.paymentrequired": "moodle",
|
||||
"core.courses.paypalaccepted": "enrol_paypal",
|
||||
"core.courses.refreshcourses": "local_moodlemobileapp",
|
||||
"core.courses.reload": "moodle",
|
||||
"core.courses.removefromfavourites": "block_myoverview",
|
||||
"core.courses.search": "moodle",
|
||||
"core.courses.searchcourses": "moodle",
|
||||
"core.courses.searchcoursesadvice": "local_moodlemobileapp",
|
||||
"core.courses.selfenrolment": "local_moodlemobileapp",
|
||||
"core.courses.sendpaymentbutton": "enrol_paypal",
|
||||
"core.courses.show": "block_myoverview",
|
||||
"core.courses.showonlyenrolled": "local_moodlemobileapp",
|
||||
"core.courses.therearecourses": "moodle",
|
||||
"core.courses.totalcoursesearchresults": "local_moodlemobileapp",
|
||||
"core.currentdevice": "local_moodlemobileapp",
|
||||
"core.custom": "form",
|
||||
"core.datastoredoffline": "local_moodlemobileapp",
|
||||
"core.date": "moodle",
|
||||
"core.datecreated": "repository",
|
||||
"core.day": "moodle",
|
||||
"core.days": "moodle",
|
||||
"core.decsep": "langconfig",
|
||||
|
@ -1567,10 +1660,12 @@
|
|||
"core.dftimedate": "local_moodlemobileapp",
|
||||
"core.digitalminor": "moodle",
|
||||
"core.digitalminor_desc": "moodle",
|
||||
"core.disablefullscreen": "h5p",
|
||||
"core.discard": "local_moodlemobileapp",
|
||||
"core.dismiss": "local_moodlemobileapp",
|
||||
"core.displayoptions": "atto_media",
|
||||
"core.done": "survey",
|
||||
"core.dontshowagain": "local_moodlemobileapp",
|
||||
"core.download": "moodle",
|
||||
"core.downloaded": "local_moodlemobileapp",
|
||||
"core.downloadfile": "moodle",
|
||||
|
@ -1592,6 +1687,7 @@
|
|||
"core.editor.underline": "atto_underline/pluginname",
|
||||
"core.editor.unorderedlist": "atto_unorderedlist/pluginname",
|
||||
"core.emptysplit": "local_moodlemobileapp",
|
||||
"core.endonesteptour": "tool_usertours",
|
||||
"core.error": "moodle",
|
||||
"core.errorchangecompletion": "local_moodlemobileapp",
|
||||
"core.errordeletefile": "local_moodlemobileapp",
|
||||
|
@ -1650,6 +1746,8 @@
|
|||
"core.forcepasswordchangenotice": "moodle",
|
||||
"core.fulllistofcourses": "moodle",
|
||||
"core.fullnameandsitename": "local_moodlemobileapp",
|
||||
"core.fullscreen": "h5p",
|
||||
"core.goto": "local_moodlemobileapp",
|
||||
"core.grades.aggregatemean": "grades",
|
||||
"core.grades.aggregatesum": "grades",
|
||||
"core.grades.average": "grades",
|
||||
|
@ -1657,8 +1755,10 @@
|
|||
"core.grades.calculatedgrade": "grades",
|
||||
"core.grades.category": "grades",
|
||||
"core.grades.contributiontocoursetotal": "grades",
|
||||
"core.grades.fail": "grades",
|
||||
"core.grades.feedback": "grades",
|
||||
"core.grades.grade": "grades",
|
||||
"core.grades.gradebook": "grades",
|
||||
"core.grades.gradeitem": "grades",
|
||||
"core.grades.gradepass": "grades",
|
||||
"core.grades.grades": "grades",
|
||||
|
@ -1667,6 +1767,7 @@
|
|||
"core.grades.nogradesreturned": "grades",
|
||||
"core.grades.nooutcome": "grades",
|
||||
"core.grades.outcome": "grades",
|
||||
"core.grades.pass": "grades",
|
||||
"core.grades.percentage": "grades",
|
||||
"core.grades.range": "grades",
|
||||
"core.grades.rank": "grades",
|
||||
|
@ -1734,6 +1835,7 @@
|
|||
"core.h5p.licensee": "h5p",
|
||||
"core.h5p.licenseextras": "h5p",
|
||||
"core.h5p.licenseversion": "h5p",
|
||||
"core.h5p.missingdependency": "h5p",
|
||||
"core.h5p.nocopyright": "h5p",
|
||||
"core.h5p.offlineDialogBody": "h5p",
|
||||
"core.h5p.offlineDialogHeader": "h5p",
|
||||
|
@ -1789,6 +1891,8 @@
|
|||
"core.loading": "moodle",
|
||||
"core.loadmore": "local_moodlemobileapp",
|
||||
"core.location": "moodle",
|
||||
"core.login.accounts": "admin",
|
||||
"core.login.add": "moodle",
|
||||
"core.login.auth_email": "auth_email/pluginname",
|
||||
"core.login.authenticating": "local_moodlemobileapp",
|
||||
"core.login.cancel": "moodle",
|
||||
|
@ -1846,7 +1950,6 @@
|
|||
"core.login.invalidurl": "scorm",
|
||||
"core.login.invalidvaluemax": "local_moodlemobileapp",
|
||||
"core.login.invalidvaluemin": "local_moodlemobileapp",
|
||||
"core.login.localmobileunexpectedresponse": "local_moodlemobileapp",
|
||||
"core.login.loggedoutssodescription": "local_moodlemobileapp",
|
||||
"core.login.login": "moodle",
|
||||
"core.login.loginbutton": "local_moodlemobileapp",
|
||||
|
@ -1875,6 +1978,7 @@
|
|||
"core.login.passwordforgotteninstructions2": "moodle",
|
||||
"core.login.passwordrequired": "local_moodlemobileapp",
|
||||
"core.login.policyaccept": "moodle",
|
||||
"core.login.policyacceptmandatory": "local_moodlemobileapp",
|
||||
"core.login.policyagree": "moodle",
|
||||
"core.login.policyagreement": "moodle",
|
||||
"core.login.policyagreementclick": "moodle",
|
||||
|
@ -1884,8 +1988,8 @@
|
|||
"core.login.recaptchaexpired": "local_moodlemobileapp",
|
||||
"core.login.recaptchaincorrect": "local_moodlemobileapp",
|
||||
"core.login.reconnect": "local_moodlemobileapp",
|
||||
"core.login.reconnectdescription": "local_moodlemobileapp",
|
||||
"core.login.reconnectssodescription": "local_moodlemobileapp",
|
||||
"core.login.removeaccount": "local_moodlemobileapp",
|
||||
"core.login.resendemail": "moodle",
|
||||
"core.login.searchby": "local_moodlemobileapp",
|
||||
"core.login.security_question": "auth",
|
||||
|
@ -1898,12 +2002,14 @@
|
|||
"core.login.sitebadgedescription": "local_moodlemobileapp",
|
||||
"core.login.sitehasredirect": "local_moodlemobileapp",
|
||||
"core.login.siteinmaintenance": "local_moodlemobileapp",
|
||||
"core.login.sitenotallowed": "local_moodlemobileapp",
|
||||
"core.login.sitepolicynotagreederror": "local_moodlemobileapp",
|
||||
"core.login.siteurl": "local_moodlemobileapp",
|
||||
"core.login.siteurlrequired": "local_moodlemobileapp",
|
||||
"core.login.startsignup": "moodle",
|
||||
"core.login.stillcantconnect": "local_moodlemobileapp",
|
||||
"core.login.supplyinfo": "moodle",
|
||||
"core.login.toggleremove": "local_moodlemobileapp",
|
||||
"core.login.username": "moodle",
|
||||
"core.login.usernameoremail": "moodle",
|
||||
"core.login.usernamerequired": "local_moodlemobileapp",
|
||||
|
@ -1913,23 +2019,23 @@
|
|||
"core.login.youcanstillconnectwithcredentials": "local_moodlemobileapp",
|
||||
"core.login.yourenteredsite": "local_moodlemobileapp",
|
||||
"core.lostconnection": "local_moodlemobileapp",
|
||||
"core.mainmenu.changesite": "local_moodlemobileapp",
|
||||
"core.mainmenu.help": "moodle",
|
||||
"core.mainmenu.home": "moodle",
|
||||
"core.mainmenu.logout": "moodle",
|
||||
"core.mainmenu.website": "local_moodlemobileapp",
|
||||
"core.mainmenu.switchaccount": "local_moodlemobileapp",
|
||||
"core.mainmenu.usermenutourdescription": "local_moodlemobileapp",
|
||||
"core.mainmenu.usermenutourtitle": "local_moodlemobileapp",
|
||||
"core.maxfilesize": "moodle",
|
||||
"core.maxsizeandattachments": "moodle",
|
||||
"core.min": "moodle",
|
||||
"core.mins": "moodle",
|
||||
"core.minute": "moodle",
|
||||
"core.minutes": "moodle",
|
||||
"core.misc": "admin",
|
||||
"core.mod_assign": "assign/pluginname",
|
||||
"core.mod_assignment": "assignment/pluginname",
|
||||
"core.mod_book": "book/pluginname",
|
||||
"core.mod_chat": "chat/pluginname",
|
||||
"core.mod_choice": "choice/pluginname",
|
||||
"core.mod_data": "data/pluginname",
|
||||
"core.mod_database": "data/pluginname",
|
||||
"core.mod_external-tool": "lti/pluginname",
|
||||
"core.mod_feedback": "feedback/pluginname",
|
||||
"core.mod_file": "moodle/file",
|
||||
|
@ -1937,7 +2043,6 @@
|
|||
"core.mod_forum": "forum/pluginname",
|
||||
"core.mod_glossary": "glossary/pluginname",
|
||||
"core.mod_h5pactivity": "h5pactivity/pluginname",
|
||||
"core.mod_ims": "imscp/pluginname",
|
||||
"core.mod_imscp": "imscp/pluginname",
|
||||
"core.mod_label": "label/pluginname",
|
||||
"core.mod_lesson": "lesson/pluginname",
|
||||
|
@ -1951,7 +2056,7 @@
|
|||
"core.mod_wiki": "wiki/pluginname",
|
||||
"core.mod_workshop": "workshop/pluginname",
|
||||
"core.moduleintro": "moodle",
|
||||
"core.more": "moodle",
|
||||
"core.more": "moodle/moremenu",
|
||||
"core.mygroups": "group",
|
||||
"core.name": "moodle",
|
||||
"core.needhelp": "local_moodlemobileapp",
|
||||
|
@ -1991,7 +2096,6 @@
|
|||
"core.othergroups": "group",
|
||||
"core.pagea": "moodle",
|
||||
"core.parentlanguage": "langconfig",
|
||||
"core.paymentinstant": "moodle",
|
||||
"core.percentagenumber": "local_moodlemobileapp",
|
||||
"core.phone": "moodle",
|
||||
"core.pictureof": "moodle",
|
||||
|
@ -2039,6 +2143,7 @@
|
|||
"core.resources": "moodle",
|
||||
"core.restore": "moodle",
|
||||
"core.restricted": "moodle",
|
||||
"core.resume": "local_moodlemobileapp",
|
||||
"core.retry": "local_moodlemobileapp",
|
||||
"core.save": "moodle",
|
||||
"core.savechanges": "assign",
|
||||
|
@ -2058,11 +2163,14 @@
|
|||
"core.sending": "chat",
|
||||
"core.serverconnection": "error",
|
||||
"core.settings.about": "local_moodlemobileapp",
|
||||
"core.settings.accessstatement": "access",
|
||||
"core.settings.appsettings": "local_moodlemobileapp",
|
||||
"core.settings.appversion": "local_moodlemobileapp",
|
||||
"core.settings.cannotsyncloggedout": "local_moodlemobileapp",
|
||||
"core.settings.cannotsyncoffline": "local_moodlemobileapp",
|
||||
"core.settings.cannotsyncwithoutwifi": "local_moodlemobileapp",
|
||||
"core.settings.changelanguage": "local_moodlemobileapp",
|
||||
"core.settings.changelanguagealert": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme-dark": "local_moodlemobileapp",
|
||||
"core.settings.colorscheme-light": "local_moodlemobileapp",
|
||||
|
@ -2078,12 +2186,12 @@
|
|||
"core.settings.currentlanguage": "moodle",
|
||||
"core.settings.debugdisplay": "admin",
|
||||
"core.settings.debugdisplaydescription": "local_moodlemobileapp",
|
||||
"core.settings.deletesitefiles": "local_moodlemobileapp",
|
||||
"core.settings.deletesitefilestitle": "local_moodlemobileapp",
|
||||
"core.settings.developeroptions": "local_moodlemobileapp",
|
||||
"core.settings.deviceinfo": "local_moodlemobileapp",
|
||||
"core.settings.deviceos": "local_moodlemobileapp",
|
||||
"core.settings.disableall": "message",
|
||||
"core.settings.disabled": "lesson",
|
||||
"core.settings.disallowed": "message",
|
||||
"core.settings.displayformat": "local_moodlemobileapp",
|
||||
"core.settings.enabledownloadsection": "local_moodlemobileapp",
|
||||
"core.settings.enablefirebaseanalytics": "local_moodlemobileapp",
|
||||
|
@ -2092,12 +2200,12 @@
|
|||
"core.settings.enablerichtexteditordescription": "local_moodlemobileapp",
|
||||
"core.settings.enablesyncwifi": "local_moodlemobileapp",
|
||||
"core.settings.entriesincache": "local_moodlemobileapp",
|
||||
"core.settings.errordeletesitefiles": "local_moodlemobileapp",
|
||||
"core.settings.errorsyncsite": "local_moodlemobileapp",
|
||||
"core.settings.estimatedfreespace": "local_moodlemobileapp",
|
||||
"core.settings.filesystemroot": "local_moodlemobileapp",
|
||||
"core.settings.fontsize": "local_moodlemobileapp",
|
||||
"core.settings.fontsizecharacter": "block_accessibility/char",
|
||||
"core.settings.forced": "message",
|
||||
"core.settings.forcedsetting": "local_moodlemobileapp",
|
||||
"core.settings.general": "moodle",
|
||||
"core.settings.helpusimprove": "local_moodlemobileapp",
|
||||
|
@ -2107,7 +2215,6 @@
|
|||
"core.settings.license": "moodle",
|
||||
"core.settings.localnotifavailable": "local_moodlemobileapp",
|
||||
"core.settings.locationhref": "local_moodlemobileapp",
|
||||
"core.settings.locked": "admin",
|
||||
"core.settings.loggedin": "message",
|
||||
"core.settings.loggedoff": "message",
|
||||
"core.settings.navigatorlanguage": "local_moodlemobileapp",
|
||||
|
@ -2125,13 +2232,13 @@
|
|||
"core.settings.siteinfo": "local_moodlemobileapp",
|
||||
"core.settings.sites": "moodle",
|
||||
"core.settings.spaceusage": "local_moodlemobileapp",
|
||||
"core.settings.spaceusagehelp": "local_moodlemobileapp",
|
||||
"core.settings.synchronization": "local_moodlemobileapp",
|
||||
"core.settings.synchronizenow": "local_moodlemobileapp",
|
||||
"core.settings.synchronizenowhelp": "local_moodlemobileapp",
|
||||
"core.settings.syncsettings": "local_moodlemobileapp",
|
||||
"core.settings.total": "moodle",
|
||||
"core.settings.wificonnection": "local_moodlemobileapp",
|
||||
"core.settings.youradev": "local_moodlemobileapp",
|
||||
"core.sharedfiles.chooseaccountstorefile": "local_moodlemobileapp",
|
||||
"core.sharedfiles.chooseactionrepeatedfile": "local_moodlemobileapp",
|
||||
"core.sharedfiles.errorreceivefilenosites": "local_moodlemobileapp",
|
||||
|
@ -2149,6 +2256,7 @@
|
|||
"core.sitehome.sitehome": "moodle",
|
||||
"core.sitehome.sitenews": "moodle",
|
||||
"core.sitemaintenance": "admin",
|
||||
"core.size": "moodle",
|
||||
"core.sizeb": "moodle",
|
||||
"core.sizegb": "moodle",
|
||||
"core.sizekb": "moodle",
|
||||
|
@ -2158,7 +2266,7 @@
|
|||
"core.sorry": "local_moodlemobileapp",
|
||||
"core.sort": "moodle",
|
||||
"core.sortby": "moodle",
|
||||
"core.start": "grouptool",
|
||||
"core.start": "local_moodlemobileapp",
|
||||
"core.storingfiles": "local_moodlemobileapp",
|
||||
"core.strftimedate": "langconfig",
|
||||
"core.strftimedatefullshort": "langconfig",
|
||||
|
@ -2177,6 +2285,8 @@
|
|||
"core.strftimetime24": "langconfig",
|
||||
"core.submit": "moodle",
|
||||
"core.success": "moodle",
|
||||
"core.summary": "moodle",
|
||||
"core.swipenavigationtourdescription": "local_moodlemobileapp",
|
||||
"core.tablet": "local_moodlemobileapp",
|
||||
"core.tag.defautltagcoll": "tag",
|
||||
"core.tag.errorareanotsupported": "local_moodlemobileapp",
|
||||
|
@ -2203,6 +2313,7 @@
|
|||
"core.toggledelete": "local_moodlemobileapp",
|
||||
"core.tryagain": "local_moodlemobileapp",
|
||||
"core.twoparagraphs": "local_moodlemobileapp",
|
||||
"core.type": "repository",
|
||||
"core.uhoh": "local_moodlemobileapp",
|
||||
"core.unexpectederror": "local_moodlemobileapp",
|
||||
"core.unicodenotsupported": "local_moodlemobileapp",
|
||||
|
@ -2234,26 +2345,32 @@
|
|||
"core.user.participants": "moodle",
|
||||
"core.user.phone1": "moodle",
|
||||
"core.user.phone2": "moodle",
|
||||
"core.user.profile": "moodle",
|
||||
"core.user.roles": "moodle",
|
||||
"core.user.sendemail": "local_moodlemobileapp",
|
||||
"core.user.student": "moodle/defaultcoursestudent",
|
||||
"core.user.teacher": "moodle/noneditingteacher",
|
||||
"core.user.useraccount": "moodle",
|
||||
"core.user.userwithid": "local_moodlemobileapp",
|
||||
"core.user.webpage": "moodle",
|
||||
"core.userdeleted": "moodle",
|
||||
"core.userdetails": "moodle",
|
||||
"core.usernologin": "local_moodlemobileapp",
|
||||
"core.usernotfullysetup": "error",
|
||||
"core.users": "moodle",
|
||||
"core.usersuspended": "tool_reportbuilder",
|
||||
"core.view": "moodle",
|
||||
"core.viewcode": "local_moodlemobileapp",
|
||||
"core.vieweditor": "local_moodlemobileapp",
|
||||
"core.viewembeddedcontent": "local_moodlemobileapp",
|
||||
"core.viewprofile": "moodle",
|
||||
"core.warningofflinedatadeleted": "local_moodlemobileapp",
|
||||
"core.warnopeninbrowser": "local_moodlemobileapp",
|
||||
"core.week": "moodle",
|
||||
"core.weeks": "moodle",
|
||||
"core.whatisyourage": "moodle",
|
||||
"core.wheredoyoulive": "moodle",
|
||||
"core.whoissiteadmin": "local_moodlemobileapp",
|
||||
"core.whoops": "local_moodlemobileapp",
|
||||
"core.whyisthishappening": "local_moodlemobileapp",
|
||||
"core.whyisthisrequired": "moodle",
|
||||
"core.wsfunctionnotavailable": "local_moodlemobileapp",
|
||||
|
@ -2261,5 +2378,7 @@
|
|||
"core.years": "moodle",
|
||||
"core.yes": "moodle",
|
||||
"core.youreoffline": "local_moodlemobileapp",
|
||||
"core.youreonline": "local_moodlemobileapp"
|
||||
"core.youreonline": "local_moodlemobileapp",
|
||||
"core.zoomin": "local_moodlemobileapp",
|
||||
"core.zoomout": "local_moodlemobileapp"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// (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.
|
||||
|
||||
const { readdirSync, readFileSync } = require('fs');
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.error('Missing measure timings storage path argument');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const performanceMeasuresStoragePath = process.argv[2].trimRight('/') + '/';
|
||||
const files = readdirSync(performanceMeasuresStoragePath);
|
||||
const performanceMeasures = {};
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No logs found!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Aggregate data
|
||||
for (const file of files) {
|
||||
const performanceMeasure = JSON.parse(readFileSync(performanceMeasuresStoragePath + file));
|
||||
|
||||
performanceMeasures[performanceMeasure.name] = performanceMeasures[performanceMeasure.name] ?? {
|
||||
duration: [],
|
||||
scripting: [],
|
||||
styling: [],
|
||||
blocking: [],
|
||||
longTasks: [],
|
||||
database: [],
|
||||
networking: [],
|
||||
};
|
||||
performanceMeasures[performanceMeasure.name].duration.push(performanceMeasure.duration);
|
||||
performanceMeasures[performanceMeasure.name].scripting.push(performanceMeasure.scripting);
|
||||
performanceMeasures[performanceMeasure.name].styling.push(performanceMeasure.styling);
|
||||
performanceMeasures[performanceMeasure.name].blocking.push(performanceMeasure.blocking);
|
||||
performanceMeasures[performanceMeasure.name].longTasks.push(performanceMeasure.longTasks);
|
||||
performanceMeasures[performanceMeasure.name].database.push(performanceMeasure.database);
|
||||
performanceMeasures[performanceMeasure.name].networking.push(performanceMeasure.networking);
|
||||
}
|
||||
|
||||
// Calculate averages
|
||||
for (const [name, { duration, scripting, styling, blocking, longTasks, database, networking }] of Object.entries(performanceMeasures)) {
|
||||
const totalRuns = duration.length;
|
||||
const averageDuration = Math.round(duration.reduce((total, duration) => total + duration) / totalRuns);
|
||||
const averageScripting = Math.round(scripting.reduce((total, scripting) => total + scripting) / totalRuns);
|
||||
const averageStyling = Math.round(styling.reduce((total, styling) => total + styling) / totalRuns);
|
||||
const averageBlocking = Math.round(blocking.reduce((total, blocking) => total + blocking) / totalRuns);
|
||||
const averageLongTasks = Math.round(longTasks.reduce((total, longTasks) => total + longTasks) / totalRuns);
|
||||
const averageDatabase = Math.round(database.reduce((total, database) => total + database) / totalRuns);
|
||||
const averageNetworking = Math.round(networking.reduce((total, networking) => total + networking) / totalRuns);
|
||||
|
||||
performanceMeasures[name] = {
|
||||
'Total duration': `${averageDuration}ms`,
|
||||
'Scripting': `${averageScripting}ms`,
|
||||
'Styling': `${averageStyling}ms`,
|
||||
'Blocking': `${averageBlocking}ms`,
|
||||
'# Network requests': averageNetworking,
|
||||
'# DB Queries': averageDatabase,
|
||||
'# Long Tasks': averageLongTasks,
|
||||
'# runs': totalRuns,
|
||||
};
|
||||
}
|
||||
|
||||
// Sort tests
|
||||
const tests = Object.keys(performanceMeasures).sort();
|
||||
const sortedPerformanceMeasures = {};
|
||||
|
||||
for (const test of tests) {
|
||||
sortedPerformanceMeasures[test] = performanceMeasures[test];
|
||||
}
|
||||
|
||||
// Display data
|
||||
console.table(sortedPerformanceMeasures);
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script is necessary because @ionic/cli is passing one argument to the ionic:serve hook
|
||||
# that is unsupported by angular cli: https://github.com/ionic-team/ionic-cli/issues/4743
|
||||
#
|
||||
# Once the issue is fixed, this script can be replaced adding the following npm script:
|
||||
#
|
||||
# "ionic:serve": "gulp watch & NODE_OPTIONS=--max-old-space-size=4096 ng serve"
|
||||
#
|
||||
|
||||
# Run gulp watch.
|
||||
echo "> gulp watch &"
|
||||
gulp watch &
|
||||
|
||||
# Remove unknown arguments and prepare angular target.
|
||||
args=("$@")
|
||||
angulartarget="serve"
|
||||
total=${#args[@]}
|
||||
for ((i=0; i<total; ++i)); do
|
||||
case ${args[i]} in
|
||||
--project=*)
|
||||
unset args[i];
|
||||
;;
|
||||
--platform=*)
|
||||
angulartarget="ionic-cordova-serve";
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Serve app.
|
||||
echo "> NODE_OPTIONS=--max-old-space-size=4096 ng run app:$angulartarget ${args[@]}"
|
||||
NODE_OPTIONS=--max-old-space-size=4096 ng run "app:$angulartarget" ${args[@]}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace local_moodleappbehat\privacy;
|
||||
|
||||
use core_privacy\local\metadata\null_provider;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
class provider implements null_provider {
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public static function get_reason() : string {
|
||||
return 'privacy_metadata';
|
||||
}
|
||||
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
<?php
|
||||
|
||||
$string['pluginname'] = 'Moodle App Behat (auto-generated)';
|
||||
$string['privacy_metadata'] = 'This plugin should only be used in development environments, and it does not store any user data.';
|
||||
|
|
|
@ -10,7 +10,7 @@ source "lang_functions.sh"
|
|||
forceLang=$1
|
||||
|
||||
print_title 'Getting local mobile langs'
|
||||
git clone --depth 1 https://github.com/moodlehq/moodle-local_moodlemobileapp.git ../../moodle-local_moodlemobileapp
|
||||
git clone --branch integration --depth 1 https://github.com/moodlehq/moodle-local_moodlemobileapp.git ../../moodle-local_moodlemobileapp
|
||||
|
||||
if [ -z $forceLang ]; then
|
||||
get_languages
|
||||
|
|
|
@ -30,6 +30,7 @@ import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module';
|
|||
import { AddonQbehaviourModule } from './qbehaviour/qbehaviour.module';
|
||||
import { AddonQtypeModule } from './qtype/qtype.module';
|
||||
import { AddonRemoteThemesModule } from './remotethemes/remotethemes.module';
|
||||
import { AddonReportModule } from './report/report.module';
|
||||
import { AddonStorageManagerModule } from './storagemanager/storagemanager.module';
|
||||
import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module';
|
||||
|
||||
|
@ -51,6 +52,7 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield
|
|||
AddonQbehaviourModule,
|
||||
AddonQtypeModule,
|
||||
AddonRemoteThemesModule,
|
||||
AddonReportModule,
|
||||
AddonStorageManagerModule,
|
||||
AddonUserProfileFieldModule,
|
||||
],
|
||||
|
|
|
@ -44,8 +44,7 @@ const mainMenuRoutes: Routes = [
|
|||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => () => {
|
||||
useValue: () => {
|
||||
CoreContentLinksDelegate.registerHandler(AddonBadgesMyBadgesLinkHandler.instance);
|
||||
CoreContentLinksDelegate.registerHandler(AddonBadgesBadgeLinkHandler.instance);
|
||||
CoreUserDelegate.registerHandler(AddonBadgesUserHandler.instance);
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
// (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 { Params } from '@angular/router';
|
||||
import { CoreRoutedItemsManagerSource } from '@classes/items-management/routed-items-manager-source';
|
||||
import { AddonBadges, AddonBadgesUserBadge } from '../services/badges';
|
||||
|
||||
/**
|
||||
* Provides a collection of user badges.
|
||||
*/
|
||||
export class AddonBadgesUserBadgesSource extends CoreRoutedItemsManagerSource<AddonBadgesUserBadge> {
|
||||
|
||||
readonly COURSE_ID: number;
|
||||
readonly USER_ID: number;
|
||||
|
||||
constructor(courseId: number, userId: number) {
|
||||
super();
|
||||
|
||||
this.COURSE_ID = courseId;
|
||||
this.USER_ID = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getItemPath(badge: AddonBadgesUserBadge): string {
|
||||
return badge.uniquehash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getItemQueryParams(): Params {
|
||||
return {
|
||||
courseId: this.COURSE_ID,
|
||||
userId: this.USER_ID,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async loadPageItems(): Promise<{ items: AddonBadgesUserBadge[] }> {
|
||||
const badges = await AddonBadges.getUserBadges(this.COURSE_ID, this.USER_ID);
|
||||
|
||||
return { items: badges };
|
||||
}
|
||||
|
||||
}
|
|
@ -20,10 +20,10 @@
|
|||
"issuerurl": "Issuer URL",
|
||||
"language": "Language",
|
||||
"noalignment": "This badge does not have any external skills or standards specified.",
|
||||
"nobadges": "There are no badges available.",
|
||||
"nobadges": "There are currently no badges available for users to earn.",
|
||||
"norelated": "This badge does not have any related badges.",
|
||||
"recipientdetails": "Recipient details",
|
||||
"relatedbages": "Related badges",
|
||||
"version": "Version",
|
||||
"warnexpired": "(This badge has expired!)"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,13 @@
|
|||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1 *ngIf="badge">{{ badge.name }}</h1>
|
||||
<h1 *ngIf="!badge">{{ 'addon.badges.badges' | translate }}</h1>
|
||||
<ion-title>
|
||||
<h1 *ngIf="badge">{{ badge.name }}</h1>
|
||||
<h1 *ngIf="!badge">{{ 'addon.badges.badges' | translate }}</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-content [core-swipe-navigation]="badges" class="limited-width">
|
||||
<ion-refresher slot="fixed" [disabled]="!badgeLoaded" (ionRefresh)="refreshBadges($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
@ -53,7 +55,9 @@
|
|||
<ion-item class="ion-text-wrap" *ngIf="badge.issuercontact">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.contact' | translate}}</h2>
|
||||
<p><a href="mailto:{{badge.issuercontact}}" core-link auto-login="no"> {{ badge.issuercontact }} </a></p>
|
||||
<p><a href="mailto:{{badge.issuercontact}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.issuercontact }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
@ -97,7 +101,9 @@
|
|||
<ion-item class="ion-text-wrap" *ngIf="badge.imageauthoremail">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.imageauthoremail' | translate}}</h2>
|
||||
<p><a href="mailto:{{badge.imageauthoremail}}" core-link auto-login="no"> {{ badge.imageauthoremail }} </a></p>
|
||||
<p><a href="mailto:{{badge.imageauthoremail}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.imageauthoremail }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.imageauthorurl">
|
||||
|
@ -153,7 +159,9 @@
|
|||
<!-- Endorsement -->
|
||||
<ion-item-group *ngIf="badge.endorsement">
|
||||
<ion-item-divider>
|
||||
<ion-label><h2>{{ 'addon.badges.bendorsement' | translate}}</h2></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.bendorsement' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.endorsement.issuername">
|
||||
<ion-label>
|
||||
|
@ -165,7 +173,7 @@
|
|||
<ion-label>
|
||||
<h2>{{ 'addon.badges.issueremail' | translate}}</h2>
|
||||
<p>
|
||||
<a href="mailto:{{badge.endorsement.issueremail}}" core-link auto-login="no">
|
||||
<a href="mailto:{{badge.endorsement.issueremail}}" core-link auto-login="no" [showBrowserWarning]="false">
|
||||
{{ badge.endorsement.issueremail }}
|
||||
</a>
|
||||
</p>
|
||||
|
@ -200,27 +208,39 @@
|
|||
<!-- Related badges -->
|
||||
<ion-item-group *ngIf="badge.relatedbadges">
|
||||
<ion-item-divider>
|
||||
<ion-label><h2>{{ 'addon.badges.relatedbages' | translate}}</h2></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.relatedbages' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let relatedBadge of badge.relatedbadges">
|
||||
<ion-label><h2>{{ relatedBadge.name }}</h2></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ relatedBadge.name }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.relatedbadges.length == 0">
|
||||
<ion-label><h2>{{ 'addon.badges.norelated' | translate}}</h2></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.norelated' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
|
||||
<!-- Competencies alignment -->
|
||||
<ion-item-group *ngIf="badge.alignment">
|
||||
<ion-item-divider>
|
||||
<ion-label><h2>{{ 'addon.badges.alignment' | translate}}</h2></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.alignment' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<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></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ alignment.targetname }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="badge.alignment.length == 0">
|
||||
<ion-label><h2>{{ 'addon.badges.noalignment' | translate}}</h2></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.badges.noalignment' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ng-container>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
|
@ -23,6 +23,9 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { CoreSwipeNavigationItemsManager } from '@classes/items-management/swipe-navigation-items-manager';
|
||||
import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source';
|
||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||
|
||||
/**
|
||||
* Page that displays the list of calendar events.
|
||||
|
@ -31,7 +34,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||
selector: 'page-addon-badges-issued-badge',
|
||||
templateUrl: 'issued-badge.html',
|
||||
})
|
||||
export class AddonBadgesIssuedBadgePage implements OnInit {
|
||||
export class AddonBadgesIssuedBadgePage implements OnInit, OnDestroy {
|
||||
|
||||
protected badgeHash = '';
|
||||
protected userId!: number;
|
||||
|
@ -40,24 +43,39 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
|||
user?: CoreUserProfile;
|
||||
course?: CoreEnrolledCourseData;
|
||||
badge?: AddonBadgesUserBadge;
|
||||
badges: CoreSwipeNavigationItemsManager;
|
||||
badgeLoaded = false;
|
||||
currentTime = 0;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
) { }
|
||||
constructor(protected route: ActivatedRoute) {
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges.
|
||||
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getRequiredCurrentSite().getUserId();
|
||||
this.badgeHash = CoreNavigator.getRouteParam('badgeHash') || '';
|
||||
|
||||
const source = CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(
|
||||
AddonBadgesUserBadgesSource,
|
||||
[this.courseId, this.userId],
|
||||
);
|
||||
|
||||
this.badges = new CoreSwipeNavigationItemsManager(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.courseId = CoreNavigator.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges.
|
||||
this.userId = CoreNavigator.getRouteNumberParam('userId') || CoreSites.getCurrentSite()!.getUserId();
|
||||
this.badgeHash = CoreNavigator.getRouteParam('badgeHash') || '';
|
||||
|
||||
this.fetchIssuedBadge().finally(() => {
|
||||
this.badgeLoaded = true;
|
||||
});
|
||||
|
||||
this.badges.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.badges.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1>{{ 'addon.badges.badges' | translate }}</h1>
|
||||
<ion-title>
|
||||
<h1>{{ 'addon.badges.badges' | translate }}</h1>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
|
@ -12,8 +14,7 @@
|
|||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="badges.loaded">
|
||||
<core-empty-box *ngIf="badges.empty" icon="fas-trophy"
|
||||
[message]="'addon.badges.nobadges' | translate">
|
||||
<core-empty-box *ngIf="badges.empty" icon="fas-trophy" [message]="'addon.badges.nobadges' | translate">
|
||||
</core-empty-box>
|
||||
|
||||
<ion-list *ngIf="!badges.empty" class="ion-no-margin">
|
||||
|
|
|
@ -19,10 +19,11 @@ import { CoreTimeUtils } from '@services/utils/time';
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CorePageItemsListManager } from '@classes/page-items-list-manager';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreListItemsManager } from '@classes/items-management/list-items-manager';
|
||||
import { AddonBadgesUserBadgesSource } from '@addons/badges/classes/user-badges-source';
|
||||
import { CoreRoutedItemsManagerSourcesTracker } from '@classes/items-management/routed-items-manager-sources-tracker';
|
||||
|
||||
/**
|
||||
* Page that displays the list of calendar events.
|
||||
|
@ -34,15 +35,23 @@ import { CoreNavigator } from '@services/navigator';
|
|||
export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
||||
|
||||
currentTime = 0;
|
||||
badges: AddonBadgesUserBadgesManager;
|
||||
badges: CoreListItemsManager<AddonBadgesUserBadge, AddonBadgesUserBadgesSource>;
|
||||
|
||||
@ViewChild(CoreSplitViewComponent) splitView!: CoreSplitViewComponent;
|
||||
|
||||
constructor() {
|
||||
const courseId = CoreNavigator.getRouteNumberParam('courseId') ?? 0; // Use 0 for site badges.
|
||||
let courseId = CoreNavigator.getRouteNumberParam('courseId') ?? 0; // Use 0 for site badges.
|
||||
const userId = CoreNavigator.getRouteNumberParam('userId') ?? CoreSites.getCurrentSiteUserId();
|
||||
|
||||
this.badges = new AddonBadgesUserBadgesManager(AddonBadgesUserBadgesPage, courseId, userId);
|
||||
if (courseId === CoreSites.getCurrentSiteHomeId()) {
|
||||
// Use courseId 0 for site home, otherwise the site doesn't return site badges.
|
||||
courseId = 0;
|
||||
}
|
||||
|
||||
this.badges = new CoreListItemsManager(
|
||||
CoreRoutedItemsManagerSourcesTracker.getOrCreateSource(AddonBadgesUserBadgesSource, [courseId, userId]),
|
||||
AddonBadgesUserBadgesPage,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,8 +76,13 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
|||
* @param refresher Refresher.
|
||||
*/
|
||||
async refreshBadges(refresher?: IonRefresher): Promise<void> {
|
||||
await CoreUtils.ignoreErrors(AddonBadges.invalidateUserBadges(this.badges.courseId, this.badges.userId));
|
||||
await CoreUtils.ignoreErrors(this.fetchBadges());
|
||||
await CoreUtils.ignoreErrors(
|
||||
AddonBadges.invalidateUserBadges(
|
||||
this.badges.getSource().COURSE_ID,
|
||||
this.badges.getSource().USER_ID,
|
||||
),
|
||||
);
|
||||
await CoreUtils.ignoreErrors(this.badges.reload());
|
||||
|
||||
refresher?.complete();
|
||||
}
|
||||
|
@ -80,55 +94,12 @@ export class AddonBadgesUserBadgesPage implements AfterViewInit, OnDestroy {
|
|||
this.currentTime = CoreTimeUtils.timestamp();
|
||||
|
||||
try {
|
||||
await this.fetchBadges();
|
||||
await this.badges.reload();
|
||||
} catch (message) {
|
||||
CoreDomUtils.showErrorModalDefault(message, 'Error loading badges');
|
||||
|
||||
this.badges.setItems([]);
|
||||
this.badges.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of badges.
|
||||
*/
|
||||
private async fetchBadges(): Promise<void> {
|
||||
const badges = await AddonBadges.getUserBadges(this.badges.courseId, this.badges.userId);
|
||||
|
||||
this.badges.setItems(badges);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to manage badges.
|
||||
*/
|
||||
class AddonBadgesUserBadgesManager extends CorePageItemsListManager<AddonBadgesUserBadge> {
|
||||
|
||||
courseId: number;
|
||||
userId: number;
|
||||
|
||||
constructor(pageComponent: unknown, courseId: number, userId: number) {
|
||||
super(pageComponent);
|
||||
|
||||
this.courseId = courseId;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemPath(badge: AddonBadgesUserBadge): string {
|
||||
return badge.uniquehash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected getItemQueryParams(): Params {
|
||||
return {
|
||||
courseId: this.courseId,
|
||||
userId: this.userId,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ export class AddonBadgesProvider {
|
|||
async isPluginEnabled(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
return site.canUseAdvancedFeature('enablebadges') && site.wsAvailable('core_course_get_user_navigation_options');
|
||||
return site.canUseAdvancedFeature('enablebadges');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +83,7 @@ export class AddonBadgesProvider {
|
|||
badge.alignment = badge.alignment || badge.competencies;
|
||||
|
||||
// Check that the alignment is valid, they were broken in 3.7.
|
||||
if (badge.alignment && badge.alignment[0] && typeof badge.alignment[0].targetname == 'undefined') {
|
||||
if (badge.alignment && badge.alignment[0] && badge.alignment[0].targetname === undefined) {
|
||||
// If any badge lacks targetname it means they are affected by the Moodle bug, don't display them.
|
||||
delete badge.alignment;
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ export type AddonBadgesUserBadge = {
|
|||
targetframework?: string; // Target framework.
|
||||
targetcode?: string; // Target code.
|
||||
}[];
|
||||
competencies?: { // @deprecated from 3.7. @since 3.6. In 3.7 it was renamed to alignment.
|
||||
competencies?: { // @deprecatedonmoodle from 3.7. @since 3.6. In 3.7 it was renamed to alignment.
|
||||
id?: number; // Alignment id.
|
||||
badgeid?: number; // Badge id.
|
||||
targetname?: string; // Target name.
|
||||
|
|
|
@ -14,8 +14,14 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
|
||||
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
|
||||
import {
|
||||
CoreUserDelegateContext,
|
||||
CoreUserDelegateService,
|
||||
CoreUserProfileHandler,
|
||||
CoreUserProfileHandlerData,
|
||||
} from '@features/user/services/user-delegate';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonBadges } from '../badges';
|
||||
|
||||
|
@ -25,52 +31,58 @@ import { AddonBadges } from '../badges';
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonBadgesUserHandlerService implements CoreUserProfileHandler {
|
||||
|
||||
name = 'AddonBadges';
|
||||
priority = 50;
|
||||
name = 'AddonBadges:fakename'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
||||
priority = 300;
|
||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
||||
|
||||
/**
|
||||
* Check if handler is enabled.
|
||||
*
|
||||
* @return Always enabled.
|
||||
* @inheritdoc
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return AddonBadges.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if handler is enabled for this user in this context.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
|
||||
* @return True if enabled, false otherwise.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabledForCourse(
|
||||
async isEnabledForContext(
|
||||
context: CoreUserDelegateContext,
|
||||
courseId: number,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
): Promise<boolean> {
|
||||
if (navOptions && typeof navOptions.badges != 'undefined') {
|
||||
// Check if feature is disabled.
|
||||
const currentSite = CoreSites.getCurrentSite();
|
||||
if (!currentSite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context === CoreUserDelegateContext.USER_MENU) {
|
||||
if (currentSite.isFeatureDisabled('CoreUserDelegate_AddonBadges:account')) {
|
||||
return false;
|
||||
}
|
||||
} else if (currentSite.isFeatureDisabled('CoreUserDelegate_AddonBadges')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (navOptions && navOptions.badges !== undefined) {
|
||||
return navOptions.badges;
|
||||
}
|
||||
|
||||
// If we reach here, it means we are opening the user site profile.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data needed to render the handler.
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDisplayData(): CoreUserProfileHandlerData {
|
||||
return {
|
||||
icon: 'fas-trophy',
|
||||
title: 'addon.badges.badges',
|
||||
action: (event, user, courseId): void => {
|
||||
action: (event, user, context, contextId): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
CoreNavigator.navigateToSitePath('/badges', {
|
||||
params: { courseId, userId: user.id },
|
||||
params: { courseId: contextId, userId: user.id },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
:host {
|
||||
--mod-icon-filter: brightness(0);
|
||||
|
||||
core-mod-icon {
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
--filter: var(--mod-icon-filter);
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(body.dark) {
|
||||
--mod-icon-filter: brightness(0) invert(1);
|
||||
}
|
|
@ -21,6 +21,7 @@ import { ContextLevel, CoreConstants } from '@/core/constants';
|
|||
import { Translate } from '@singletons';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
|
||||
/**
|
||||
* Component to render an "activity modules" block.
|
||||
|
@ -28,6 +29,7 @@ import { CoreNavigator } from '@services/navigator';
|
|||
@Component({
|
||||
selector: 'addon-block-activitymodules',
|
||||
templateUrl: 'addon-block-activitymodules.html',
|
||||
styleUrls: ['activitymodules.scss'],
|
||||
})
|
||||
export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent implements OnInit {
|
||||
|
||||
|
@ -66,14 +68,14 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
|
|||
}
|
||||
|
||||
section.modules.forEach((mod) => {
|
||||
if (mod.uservisible === false || !CoreCourse.moduleHasView(mod) ||
|
||||
typeof modFullNames[mod.modname] != 'undefined') {
|
||||
if (!CoreCourseHelper.canUserViewModule(mod, section) || !CoreCourse.moduleHasView(mod) ||
|
||||
modFullNames[mod.modname] !== undefined) {
|
||||
// Ignore this module.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the archetype of the module type.
|
||||
if (typeof archetypes[mod.modname] == 'undefined') {
|
||||
if (archetypes[mod.modname] === undefined) {
|
||||
archetypes[mod.modname] = CoreCourseModuleDelegate.supportsFeature<number>(
|
||||
mod.modname,
|
||||
CoreConstants.FEATURE_MOD_ARCHETYPE,
|
||||
|
@ -96,16 +98,13 @@ export class AddonBlockActivityModulesComponent extends CoreBlockBaseComponent i
|
|||
// Sort the modnames alphabetically.
|
||||
modFullNames = CoreUtils.sortValues(modFullNames);
|
||||
for (const modName in modFullNames) {
|
||||
let icon: string;
|
||||
const iconModName = modName === 'resources' ? 'page' : modName;
|
||||
|
||||
if (modName === 'resources') {
|
||||
icon = CoreCourse.getModuleIconSrc('page', modIcons['page']);
|
||||
} else {
|
||||
icon = CoreCourseModuleDelegate.getModuleIconSrc(modName, modIcons[modName]) || '';
|
||||
}
|
||||
const icon = await CoreCourseModuleDelegate.getModuleIconSrc(iconModName, modIcons[iconModName]);
|
||||
|
||||
this.entries.push({
|
||||
icon: icon,
|
||||
icon,
|
||||
iconModName,
|
||||
name: modFullNames[modName],
|
||||
modName,
|
||||
});
|
||||
|
@ -145,4 +144,5 @@ type AddonBlockActivityModuleEntry = {
|
|||
icon: string;
|
||||
name: string;
|
||||
modName: string;
|
||||
iconModName: string;
|
||||
};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<ion-item-divider sticky="true">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.block_activitymodules.pluginname' | translate }}</h2>
|
||||
<h2>{{ 'addon.block_activitymodules.pluginname' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" [fullscreen]="false" class="margin">
|
||||
<ion-item class="ion-text-wrap item-media" *ngFor="let entry of entries" detail="true" button
|
||||
(click)="gotoCoureListModType(entry)">
|
||||
<img slot="start" [src]="entry.icon" alt="" role="presentation" class="core-module-icon">
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let entry of entries" detail="true" button (click)="gotoCoureListModType(entry)">
|
||||
<core-mod-icon slot="start" [modicon]="entry.icon" [modname]="entry.iconModName" [showAlt]="false">
|
||||
</core-mod-icon>
|
||||
<ion-label>{{ entry.name }}</ion-label>
|
||||
</ion-item>
|
||||
</core-loading>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
text-align: start;
|
||||
padding-top: .75rem;
|
||||
padding-bottom: .75rem;
|
||||
color: var(--gray-darker);
|
||||
color: var(--medium);
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,42 @@
|
|||
:host .core-block-content ::ng-deep {
|
||||
ul.badges {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
-webkit-padding-start: 0;
|
||||
:host {
|
||||
--badge-size: 100px;
|
||||
--badge-container-size: 150px;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
padding-top: 1em;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
width: 150px;
|
||||
.core-block-content ::ng-deep {
|
||||
|
||||
.badge-name {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
ul.badges {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin-top: 1em;
|
||||
vertical-align: top;
|
||||
width: var(--badge-container-size);
|
||||
|
||||
.badge-name {
|
||||
display: block;
|
||||
padding: 5px;
|
||||
}
|
||||
.badge-image {
|
||||
width: var(--badge-size);
|
||||
}
|
||||
|
||||
.expireimage {
|
||||
content: 'expired';
|
||||
background-image: url('/assets/img/expired.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: var(--badge-size) var(--badge-size);
|
||||
width: var(--badge-size);
|
||||
height: var(--badge-size);
|
||||
left: calc((var(--badge-container-size) - var(--badge-size)) /2);
|
||||
top: 0;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
opacity: .85;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import { AddonBlockCalendarMonthModule } from './calendarmonth/calendarmonth.mod
|
|||
import { AddonBlockCalendarUpcomingModule } from './calendarupcoming/calendarupcoming.module';
|
||||
import { AddonBlockCommentsModule } from './comments/comments.module';
|
||||
import { AddonBlockCompletionStatusModule } from './completionstatus/completionstatus.module';
|
||||
import { AddonBlockCourseListModule } from './courselist/courselist.module';
|
||||
import { AddonBlockGlossaryRandomModule } from './glossaryrandom/glossaryrandom.module';
|
||||
import { AddonBlockHtmlModule } from './html/html.module';
|
||||
import { AddonBlockLearningPlansModule } from './learningplans/learningplans.module';
|
||||
|
@ -53,6 +54,7 @@ import { AddonBlockTimelineModule } from './timeline/timeline.module';
|
|||
AddonBlockCalendarUpcomingModule,
|
||||
AddonBlockCommentsModule,
|
||||
AddonBlockCompletionStatusModule,
|
||||
AddonBlockCourseListModule,
|
||||
AddonBlockGlossaryRandomModule,
|
||||
AddonBlockHtmlModule,
|
||||
AddonBlockLearningPlansModule,
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
:host .core-block-content ::ng-deep {
|
||||
ul.inline-list {
|
||||
font-size: 80%;
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin: 0;
|
||||
-webkit-padding-start: 0;
|
||||
|
||||
li {
|
||||
|
@ -11,8 +9,8 @@
|
|||
display: inline-block;
|
||||
|
||||
a {
|
||||
background: var(--ion-color-primary);
|
||||
color: var(--ion-color-primary-contrast);
|
||||
background: var(--primary);
|
||||
color: var(--primary-contrast);
|
||||
padding: 3px 8px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
|
@ -24,7 +22,7 @@
|
|||
contain: content;
|
||||
vertical-align: baseline;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--small-radius);
|
||||
}
|
||||
.s20 {
|
||||
font-size: 1.5em;
|
||||
|
|
|
@ -16,10 +16,10 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
|
||||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
|
||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||
import { AddonCalendar } from '@/addons/calendar/services/calendar';
|
||||
import { CoreCourseBlock } from '@features/course/services/course';
|
||||
import { Params } from '@angular/router';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonCalendarMainMenuHandlerService } from '@addons/calendar/services/handlers/mainmenu';
|
||||
|
||||
/**
|
||||
* Block handler.
|
||||
|
@ -45,7 +45,7 @@ export class AddonBlockCalendarMonthHandlerService extends CoreBlockBaseHandler
|
|||
title: 'addon.block_calendarmonth.pluginname',
|
||||
class: 'addon-block-calendar-month',
|
||||
component: CoreBlockOnlyTitleComponent,
|
||||
link: AddonCalendar.getMainCalendarPagePath(),
|
||||
link: AddonCalendarMainMenuHandlerService.PAGE_NAME,
|
||||
linkParams: linkParams,
|
||||
navOptions: {
|
||||
preferCurrentTab: false,
|
||||
|
|
|
@ -16,10 +16,11 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
|
||||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
|
||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||
import { AddonCalendar } from '@/addons/calendar/services/calendar';
|
||||
import { CoreCourseBlock } from '@features/course/services/course';
|
||||
import { Params } from '@angular/router';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonCalendarMainMenuHandlerService } from '@addons/calendar/services/handlers/mainmenu';
|
||||
import { CoreSites } from '@services/sites';
|
||||
|
||||
/**
|
||||
* Block handler.
|
||||
|
@ -39,18 +40,18 @@ export class AddonBlockCalendarUpcomingHandlerService extends CoreBlockBaseHandl
|
|||
* @return Data or promise resolved with the data.
|
||||
*/
|
||||
getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData {
|
||||
const linkParams: Params = contextLevel == 'course' ? { courseId: instanceId } : {};
|
||||
linkParams.upcoming = true;
|
||||
const linkParams: Params = { upcoming: true };
|
||||
|
||||
if (contextLevel == 'course' && instanceId !== CoreSites.getCurrentSiteHomeId()) {
|
||||
linkParams.courseId = instanceId;
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'addon.block_calendarupcoming.pluginname',
|
||||
class: 'addon-block-calendar-upcoming',
|
||||
component: CoreBlockOnlyTitleComponent,
|
||||
link: AddonCalendar.getMainCalendarPagePath(),
|
||||
link: AddonCalendarMainMenuHandlerService.PAGE_NAME,
|
||||
linkParams: linkParams,
|
||||
navOptions: {
|
||||
preferCurrentTab: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreBlockDelegate } from '@features/block/services/block-delegate';
|
||||
import { AddonBlockCourseListHandler } from './services/block-handler';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
useValue: () => {
|
||||
CoreBlockDelegate.registerHandler(AddonBlockCourseListHandler.instance);
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AddonBlockCourseListModule {}
|
|
@ -0,0 +1,49 @@
|
|||
// (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 { CoreBlockHandlerData } from '@features/block/services/block-delegate';
|
||||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
|
||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
* Block handler.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonBlockCourseListHandlerService extends CoreBlockBaseHandler {
|
||||
|
||||
name = 'AddonBlockCourseList';
|
||||
blockName = 'course_list';
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDisplayData(): CoreBlockHandlerData {
|
||||
|
||||
return {
|
||||
title: 'core.courses.mycourses',
|
||||
class: 'addon-block-course-list',
|
||||
component: CoreBlockOnlyTitleComponent,
|
||||
link: 'courses/list',
|
||||
linkParams: { mode: 'my' },
|
||||
navOptions: {
|
||||
preferCurrentTab: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonBlockCourseListHandler = makeSingleton(AddonBlockCourseListHandlerService);
|
|
@ -17,7 +17,7 @@ import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
|
|||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
|
||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonCompetencyMainMenuHandlerService } from '@addons/competency/services/handlers/mainmenu';
|
||||
import { ADDON_COMPETENCY_LEARNING_PLANS_PAGE } from '@addons/competency/competency.module';
|
||||
|
||||
/**
|
||||
* Block handler.
|
||||
|
@ -38,7 +38,7 @@ export class AddonBlockLearningPlansHandlerService extends CoreBlockBaseHandler
|
|||
title: 'addon.block_learningplans.pluginname',
|
||||
class: 'addon-block-learning-plans',
|
||||
component: CoreBlockOnlyTitleComponent,
|
||||
link: AddonCompetencyMainMenuHandlerService.PAGE_NAME,
|
||||
link: ADDON_COMPETENCY_LEARNING_PLANS_PAGE,
|
||||
navOptions: {
|
||||
preferCurrentTab: false,
|
||||
},
|
||||
|
|
|
@ -4,97 +4,132 @@
|
|||
</ion-label>
|
||||
<div slot="end" class="flex-row">
|
||||
<!-- Download all courses. -->
|
||||
<div *ngIf="downloadCoursesEnabled && downloadEnabled && filteredCourses.length > 1 && !showFilter"
|
||||
class="core-button-spinner">
|
||||
<ion-button *ngIf="!prefetchCoursesData[selectedFilter].loading" fill="clear" color="dark" (click)="prefetchCourses()"
|
||||
[attr.aria-label]="'core.courses.downloadcourses' | translate">
|
||||
<ion-icon [name]="prefetchCoursesData[selectedFilter].icon" slot="icon-only" aria-hidden="true">
|
||||
<div *ngIf="downloadCoursesEnabled && filteredCourses.length > 0" class="core-button-spinner">
|
||||
<ion-button *ngIf="!prefetchCoursesData.loading" fill="clear" (click)="prefetchCourses()"
|
||||
[attr.aria-label]="prefetchCoursesData.statusTranslatable | translate">
|
||||
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData[selectedFilter].badge"
|
||||
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData[selectedFilter].total"
|
||||
[attr.aria-valuenow]="prefetchCoursesData[selectedFilter].count"
|
||||
[attr.aria-valuetext]="prefetchCoursesData[selectedFilter].badgeA11yText">
|
||||
{{prefetchCoursesData[selectedFilter].badge}}
|
||||
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge" role="progressbar"
|
||||
[attr.aria-valuemax]="prefetchCoursesData.total" [attr.aria-valuenow]="prefetchCoursesData.count"
|
||||
[attr.aria-valuetext]="prefetchCoursesData.badgeA11yText">
|
||||
{{prefetchCoursesData.badge}}
|
||||
</ion-badge>
|
||||
<ion-spinner *ngIf="prefetchCoursesData[selectedFilter].loading" [attr.aria-label]="'core.loading' | translate">
|
||||
<ion-spinner *ngIf="prefetchCoursesData.loading" [attr.aria-label]="'core.loading' | translate">
|
||||
</ion-spinner>
|
||||
</div>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="loaded && showFilterSwitchButton()" [priority]="1000"
|
||||
[content]="'core.courses.filtermycourses' | translate" (action)="switchFilter()" iconAction="fas-filter"
|
||||
(onClosed)="switchFilterClosed()"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="900"
|
||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.title' | translate)}}"
|
||||
(action)="switchSort('fullname')" [iconAction]="sort == 'fullname' ? 'far-dot-circle' : 'far-circle'">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && showSortFilter && showSortByShortName" [priority]="800"
|
||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.shortname' | translate)}}"
|
||||
(action)="switchSort('shortname')" [iconAction]="sort == 'shortname' ? 'far-dot-circle' : 'far-circle'">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded && showSortFilter" [priority]="700"
|
||||
content="{{('core.sortby' | translate) + ' ' + ('addon.block_myoverview.lastaccessed' | translate)}}"
|
||||
(action)="switchSort('lastaccess')" [iconAction]="sort == 'lastaccess' ? 'far-dot-circle' : 'far-circle'">
|
||||
</core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</div>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" [fullscreen]="false" class="margin">
|
||||
<div class="safe-padding-horizontal" [hidden]="showFilter || !showSelectorFilter">
|
||||
<!-- "Time" selector. -->
|
||||
<core-combobox [label]="'core.show' | translate" [selection]="selectedFilter" (onChange)="selectedChanged($event)">
|
||||
<ion-select-option class="ion-text-wrap" value="allincludinghidden" *ngIf="showFilters.allincludinghidden != 'hidden'">
|
||||
{{ 'addon.block_myoverview.allincludinghidden' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="all" *ngIf="showFilters.all != 'hidden'">
|
||||
{{ 'addon.block_myoverview.all' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="inprogress" *ngIf="showFilters.inprogress != 'hidden'"
|
||||
[disabled]="showFilters.inprogress == 'disabled'">
|
||||
{{ 'addon.block_myoverview.inprogress' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="future" *ngIf="showFilters.future != 'hidden'"
|
||||
[disabled]="showFilters.future == 'disabled'">
|
||||
{{ 'addon.block_myoverview.future' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="past" *ngIf="showFilters.past != 'hidden'"
|
||||
[disabled]="showFilters.past == 'disabled'">
|
||||
{{ 'addon.block_myoverview.past' | translate }}
|
||||
</ion-select-option>
|
||||
<ng-container *ngIf="showFilters.custom != 'hidden'">
|
||||
<ng-container *ngFor="let customOption of customFilter; let index = index">
|
||||
<ion-select-option class="ion-text-wrap" value="custom-{{index}}">{{ customOption.name }}</ion-select-option>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
|
||||
<ion-row class="ion-hide-md-up addon-block-myoverview-filter" *ngIf="hasCourses">
|
||||
<ion-col>
|
||||
<!-- Filter courses. -->
|
||||
<ion-searchbar [(ngModel)]="textFilter" (ionInput)="filterTextChanged($event.target)"
|
||||
(ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.courses.filtermycourses' | translate">
|
||||
</ion-searchbar>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row class="ion-justify-content-between ion-align-items-center addon-block-myoverview-filter" *ngIf="hasCourses">
|
||||
<ion-col size="auto" *ngIf="filters.enabled">
|
||||
<core-combobox [label]="'core.courses.filtermycourses' | translate" [selection]="filters.timeFilterSelected"
|
||||
(onChange)="filterOptionsChanged($event)">
|
||||
<ion-select-option class="ion-text-wrap core-select-option-border-bottom" value="allincludinghidden"
|
||||
*ngIf="filters.show.allincludinghidden">
|
||||
{{ 'addon.block_myoverview.allincludinghidden' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap core-select-option-border-bottom" value="all" *ngIf="filters.show.all">
|
||||
{{ 'addon.block_myoverview.all' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap"
|
||||
[class.core-select-option-border-bottom]="!filters.show.past && !filters.show.future" value="inprogress"
|
||||
*ngIf="filters.show.inprogress">
|
||||
{{ 'addon.block_myoverview.inprogress' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" [class.core-select-option-border-bottom]="!filters.show.past" value="future"
|
||||
*ngIf="filters.show.future">
|
||||
{{ 'addon.block_myoverview.future' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap core-select-option-border-bottom" value="past" *ngIf="filters.show.past">
|
||||
{{ 'addon.block_myoverview.past' | translate }}
|
||||
</ion-select-option>
|
||||
<ng-container *ngIf="filters.show.custom">
|
||||
<ng-container *ngFor="let customOption of filters.customFilters; let index = index; let last = last">
|
||||
<ion-select-option class="ion-text-wrap" value="custom-{{index}}" [class.core-select-option-border-bottom]="last">
|
||||
{{ customOption.name }}</ion-select-option>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ion-select-option class="ion-text-wrap" value="favourite" *ngIf="showFilters.favourite != 'hidden'"
|
||||
[disabled]="showFilters.favourite == 'disabled'">
|
||||
{{ 'addon.block_myoverview.favourites' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="hidden" *ngIf="showFilters.hidden != 'hidden'"
|
||||
[disabled]="showFilters.hidden == 'disabled'">
|
||||
{{ 'addon.block_myoverview.hiddencourses' | translate }}
|
||||
</ion-select-option>
|
||||
</core-combobox>
|
||||
</div>
|
||||
<ion-select-option class="ion-text-wrap core-select-option-border-bottom" value="favourite" *ngIf="filters.show.favourite">
|
||||
{{ 'addon.block_myoverview.favourites' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="hidden" *ngIf="filters.show.hidden">
|
||||
{{ 'addon.block_myoverview.hiddencourses' | translate }}
|
||||
</ion-select-option>
|
||||
</core-combobox>
|
||||
</ion-col>
|
||||
<ion-col>
|
||||
<!-- Filter courses. -->
|
||||
<ion-searchbar class="ion-hide-md-down" [(ngModel)]="textFilter" (ionInput)="filterTextChanged($event.target)"
|
||||
(ionCancel)="filterTextChanged($event.target)" [placeholder]="'core.courses.filtermycourses' | translate">
|
||||
</ion-searchbar>
|
||||
</ion-col>
|
||||
<ion-col size="auto" *ngIf="sort.enabled">
|
||||
<core-combobox [label]="'core.sortby' | translate" [selection]="sort.selected" (onChange)="sortCourses($event)"
|
||||
icon="fas-sort-amount-down-alt">
|
||||
<ion-select-option class="ion-text-wrap" value="fullname">
|
||||
{{'addon.block_myoverview.title' | translate}}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="shortname" *ngIf="sort.shortnameEnabled">
|
||||
{{'addon.block_myoverview.shortname' | translate}}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="lastaccess">
|
||||
{{'addon.block_myoverview.lastaccessed' | translate}}
|
||||
</ion-select-option>
|
||||
</core-combobox>
|
||||
</ion-col>
|
||||
<ion-col size="auto" *ngIf="isLayoutSwitcherAvailable">
|
||||
<ion-button *ngIf="layout == 'card'" fill="outline" (click)="toggleLayout('list')"
|
||||
[attr.aria-label]="'addon.block_myoverview.list' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-list" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button *ngIf="layout == 'list'" fill="outline" (click)="toggleLayout('card')"
|
||||
[attr.aria-label]="'addon.block_myoverview.card' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-th" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
||||
<!-- Filter courses. -->
|
||||
<ion-searchbar #searchbar *ngIf="showFilter" [(ngModel)]="courses.filter" (ionInput)="filterChanged($event)"
|
||||
(ionCancel)="filterChanged($event)" [placeholder]="'core.courses.filtermycourses' | translate">
|
||||
</ion-searchbar>
|
||||
|
||||
<core-empty-box *ngIf="filteredCourses.length == 0" image="assets/img/icons/courses.svg"
|
||||
[message]="'addon.block_myoverview.nocourses' | translate" inline="true">
|
||||
<core-empty-box *ngIf="filteredCourses.length == 0" image="assets/img/icons/courses.svg">
|
||||
<p *ngIf="hasCourses" class="item-heading">
|
||||
{{'addon.block_myoverview.noresult' | translate}}
|
||||
</p>
|
||||
<p *ngIf="!hasCourses" class="item-heading">
|
||||
{{'addon.block_myoverview.nocoursesenrolled' | translate}}
|
||||
</p>
|
||||
<ng-container *ngIf="searchEnabled">
|
||||
<p *ngIf="hasCourses" class="subdued">
|
||||
{{'addon.block_myoverview.noresultdescription' | translate}}
|
||||
</p>
|
||||
<p *ngIf="!hasCourses" class="subdued">
|
||||
{{'addon.block_myoverview.nocoursesenrolleddescription' | translate}}
|
||||
</p>
|
||||
<ion-button (click)="openSearch()" fill="outline">
|
||||
<ion-icon name="fas-search" slot="start" aria-hidden="true">
|
||||
</ion-icon>
|
||||
{{'addon.block_myoverview.browseallcourses' | translate}}
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
</core-empty-box>
|
||||
|
||||
<!-- List of courses. -->
|
||||
<div class="safe-area-page">
|
||||
<ion-grid class="ion-no-padding">
|
||||
<div class="safe-area-padding" *ngIf="hasCourses">
|
||||
<ion-grid class="ion-no-padding" [class.core-no-grid]="layout != 'card'" [class.list-item-limited-width]="layout != 'card'">
|
||||
<ion-row class="ion-no-padding">
|
||||
<ion-col *ngFor="let course of filteredCourses" class="ion-no-padding"
|
||||
size="12" size-sm="6" size-md="6" size-lg="4" size-xl="3">
|
||||
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true"
|
||||
[showDownload]="downloadCourseEnabled && downloadEnabled">
|
||||
</core-courses-course-progress>
|
||||
<ion-col *ngFor="let course of filteredCourses" class="ion-no-padding" size="12" size-sm="6" size-md="6" size-lg="4"
|
||||
size-xl="3">
|
||||
<core-courses-course-list-item [course]="course" class="core-courseoverview" [showDownload]="downloadCourseEnabled"
|
||||
[layout]="layout">
|
||||
</core-courses-course-list-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
:host {
|
||||
ion-row.addon-block-myoverview-filter {
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
|
||||
ion-col {
|
||||
padding: 0;
|
||||
margin-right: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
ion-button,
|
||||
core-combobox ::ng-deep ion-button {
|
||||
--border-width: 0;
|
||||
--a11y-min-target-size: 40px;
|
||||
margin: 0;
|
||||
|
||||
.select-icon {
|
||||
display: none;
|
||||
}
|
||||
ion-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
core-combobox ::ng-deep ion-select {
|
||||
margin: 0;
|
||||
--a11y-min-target-size: 40px;
|
||||
}
|
||||
|
||||
ion-searchbar {
|
||||
padding: 0;
|
||||
--height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
core-empty-box {
|
||||
.item-heading {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.subdued {
|
||||
color: var(--subdued-text-color);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,18 @@
|
|||
{
|
||||
"all": "All (except removed from view)",
|
||||
"allincludinghidden": "All",
|
||||
"all": "All",
|
||||
"allincludinghidden": "All (including archived)",
|
||||
"browseallcourses": "Browse all courses",
|
||||
"card": "Card",
|
||||
"favourites": "Starred",
|
||||
"future": "Future",
|
||||
"hiddencourses": "Removed from view",
|
||||
"hiddencourses": "Archived",
|
||||
"inprogress": "In progress",
|
||||
"lastaccessed": "Last accessed",
|
||||
"nocourses": "No courses",
|
||||
"list": "List",
|
||||
"nocoursesenrolled": "You're not enrolled in any courses yet.",
|
||||
"nocoursesenrolleddescription": "Browse all available courses below and start learning.",
|
||||
"noresult": "Your search didn't match any courses.",
|
||||
"noresultdescription": "Try adjusting your filters or browse all courses below.",
|
||||
"past": "Past",
|
||||
"pluginname": "Course overview",
|
||||
"shortname": "Short name",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host .core-block-content ::ng-deep {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
@ -17,23 +19,22 @@
|
|||
list-style-type: none;
|
||||
|
||||
.user {
|
||||
float: left;
|
||||
@include float(start);
|
||||
position: relative;
|
||||
padding-bottom: 16px;
|
||||
|
||||
.core-adapted-img-container {
|
||||
display: inline;
|
||||
margin-left: 0;
|
||||
margin-right: 8px;
|
||||
@include margin-horizontal(0px, 8px);
|
||||
}
|
||||
|
||||
.userpicture {
|
||||
vertical-align: text-bottom;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
float: right;
|
||||
@include float(end);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
|
@ -48,20 +49,3 @@
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
:host-context([dir=rtl]) .core-block-content ::ng-deep {
|
||||
.list li.listentry {
|
||||
.user {
|
||||
float: right;
|
||||
|
||||
.core-adapted-img-container {
|
||||
margin-left: 8px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreBlockHandlerData } from '@features/block/services/block-delegate';
|
||||
import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-title-block/only-title-block';
|
||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||
import { AddonPrivateFilesMainMenuHandlerService } from '@/addons/privatefiles/services/handlers/mainmenu';
|
||||
import { AddonPrivateFilesUserHandlerService } from '@addons/privatefiles/services/handlers/user';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
/**
|
||||
|
@ -39,7 +39,7 @@ export class AddonBlockPrivateFilesHandlerService extends CoreBlockBaseHandler {
|
|||
title: 'addon.block_privatefiles.pluginname',
|
||||
class: 'addon-block-private-files',
|
||||
component: CoreBlockOnlyTitleComponent,
|
||||
link: AddonPrivateFilesMainMenuHandlerService.PAGE_NAME,
|
||||
link: AddonPrivateFilesUserHandlerService.PAGE_NAME,
|
||||
linkParams: { root: 'my' },
|
||||
navOptions: {
|
||||
preferCurrentTab: false,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host .core-block-content ::ng-deep {
|
||||
.activitydate, .activityhead {
|
||||
text-align: center;
|
||||
|
@ -12,14 +14,8 @@
|
|||
margin-bottom: 1em;
|
||||
|
||||
.head .date {
|
||||
float: right;
|
||||
@include float(end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context([dir=rtl]) .core-block-content ::ng-deep {
|
||||
.unlist li .head .date {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,25 @@
|
|||
<ion-item-divider sticky="true">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2>
|
||||
<h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2>
|
||||
</ion-label>
|
||||
<div slot="end" class="flex-row">
|
||||
<div *ngIf="downloadCoursesEnabled && downloadEnabled && courses && courses.length > 1" class="core-button-spinner">
|
||||
<ion-button *ngIf="prefetchCoursesData.icon && !prefetchCoursesData.loading" fill="clear" color="dark"
|
||||
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
|
||||
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge"
|
||||
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData.total"
|
||||
[attr.aria-valuenow]="prefetchCoursesData.count" [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText">
|
||||
{{prefetchCoursesData.badge}}
|
||||
</ion-badge>
|
||||
<ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading"
|
||||
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
</div>
|
||||
|
||||
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
|
||||
</core-horizontal-scroll-controls>
|
||||
</div>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" [fullscreen]="false" class="safe-area-page margin">
|
||||
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg"
|
||||
[message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box>
|
||||
<!-- List of courses. -->
|
||||
<div
|
||||
[id]="scrollElementId"
|
||||
class="core-horizontal-scroll"
|
||||
(scroll)="scrollControls.updateScrollPosition()"
|
||||
>
|
||||
<div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll"
|
||||
(scroll)="scrollControls.updateScrollPosition()">
|
||||
<div (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
|
||||
<div class="safe-area-pseudo-padding-start"></div>
|
||||
<ng-container *ngFor="let course of courses">
|
||||
<core-courses-course-progress [course]="course" class="core-recentlyaccessedcourses"
|
||||
[showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress>
|
||||
<core-courses-course-list-item [course]="course" class="core-recentlyaccessedcourses" layout="summarycard">
|
||||
</core-courses-course-list-item>
|
||||
</ng-container>
|
||||
<div class="safe-area-pseudo-padding-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
</core-loading>
|
||||
|
|
|
@ -12,17 +12,25 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
|
||||
import {
|
||||
CoreCoursesProvider,
|
||||
CoreCoursesMyCoursesUpdatedEventData,
|
||||
CoreCourses,
|
||||
CoreCourseSummaryData,
|
||||
} from '@features/courses/services/courses';
|
||||
import {
|
||||
CoreCourseSearchedDataWithExtraInfoAndOptions,
|
||||
CoreCoursesHelper,
|
||||
CoreEnrolledCourseDataWithOptions,
|
||||
} from '@features/courses/services/courses-helper';
|
||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
|
||||
import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
|
||||
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreSite } from '@classes/site';
|
||||
|
||||
/**
|
||||
* Component to render a recent courses block.
|
||||
|
@ -31,36 +39,25 @@ import { CoreDomUtils } from '@services/utils/dom';
|
|||
selector: 'addon-block-recentlyaccessedcourses',
|
||||
templateUrl: 'addon-block-recentlyaccessedcourses.html',
|
||||
})
|
||||
export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
|
||||
export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() downloadEnabled = false;
|
||||
courses: AddonBlockRecentlyAccessedCourse[] = [];
|
||||
|
||||
courses: CoreEnrolledCourseDataWithOptions [] = [];
|
||||
prefetchCoursesData: CorePrefetchStatusInfo = {
|
||||
icon: '',
|
||||
statusTranslatable: 'core.loading',
|
||||
status: '',
|
||||
loading: true,
|
||||
badge: '',
|
||||
};
|
||||
|
||||
downloadCourseEnabled = false;
|
||||
downloadCoursesEnabled = false;
|
||||
scrollElementId!: string;
|
||||
|
||||
protected prefetchIconsInitialized = false;
|
||||
protected site!: CoreSite;
|
||||
protected isDestroyed = false;
|
||||
protected coursesObserver?: CoreEventObserver;
|
||||
protected updateSiteObserver?: CoreEventObserver;
|
||||
protected courseIds = [];
|
||||
protected fetchContentDefaultError = 'Error getting recent courses data.';
|
||||
|
||||
constructor() {
|
||||
super('AddonBlockRecentlyAccessedCoursesComponent');
|
||||
|
||||
this.site = CoreSites.getRequiredCurrentSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Generate unique id for scroll element.
|
||||
|
@ -68,175 +65,149 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
|
|||
|
||||
this.scrollElementId = `addon-block-recentlyaccessedcourses-scroll-${scrollId}`;
|
||||
|
||||
// Refresh the enabled flags if enabled.
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
|
||||
this.coursesObserver = CoreEvents.on(
|
||||
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
|
||||
(data) => {
|
||||
|
||||
if (this.shouldRefreshOnUpdatedEvent(data)) {
|
||||
this.refreshCourseList();
|
||||
}
|
||||
this.refreshCourseList(data);
|
||||
},
|
||||
|
||||
CoreSites.getCurrentSiteId(),
|
||||
this.site.getId(),
|
||||
);
|
||||
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||
if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
|
||||
// Download all courses is enabled now, initialize it.
|
||||
this.initPrefetchCoursesIcons();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the invalidate content function.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async invalidateContent(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
const courseIds = this.courses.map((course) => course.id);
|
||||
|
||||
promises.push(CoreCourses.invalidateUserCourses().finally(() =>
|
||||
// Invalidate course completion data.
|
||||
CoreUtils.allPromises(this.courseIds.map((courseId) =>
|
||||
AddonCourseCompletion.invalidateCourseCompletion(courseId)))));
|
||||
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||
if (this.courseIds.length > 0) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds.join(',')));
|
||||
}
|
||||
|
||||
await CoreUtils.allPromises(promises).finally(() => {
|
||||
this.prefetchIconsInitialized = false;
|
||||
});
|
||||
await this.invalidateCourses(courseIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the courses for recent courses.
|
||||
* Invalidate list of courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async invalidateCourseList(): Promise<void> {
|
||||
return this.site.isVersionGreaterEqualThan('3.8')
|
||||
? CoreCourses.invalidateRecentCourses()
|
||||
: CoreCourses.invalidateUserCourses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to invalidate only selected courses.
|
||||
*
|
||||
* @param courseIds Course Id array.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async invalidateCourses(courseIds: number[]): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// Invalidate course completion data.
|
||||
promises.push(this.invalidateCourseList().finally(() =>
|
||||
CoreUtils.allPromises(courseIds.map((courseId) =>
|
||||
AddonCourseCompletion.invalidateCourseCompletion(courseId)))));
|
||||
|
||||
if (courseIds.length == 1) {
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(courseIds[0]));
|
||||
} else {
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||
}
|
||||
if (courseIds.length > 0) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(',')));
|
||||
}
|
||||
|
||||
await CoreUtils.allPromises(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async fetchContent(): Promise<void> {
|
||||
const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories &&
|
||||
this.block.configsRecord.displaycategories.value == '1';
|
||||
|
||||
this.courses = await CoreCoursesHelper.getUserCoursesWithOptions('lastaccess', 10, undefined, showCategories);
|
||||
this.initPrefetchCoursesIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the list of courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async refreshCourseList(): Promise<void> {
|
||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED);
|
||||
|
||||
let recentCourses: CoreCourseSummaryData[] = [];
|
||||
try {
|
||||
await CoreCourses.invalidateUserCourses();
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
recentCourses = await CoreCourses.getRecentCourses();
|
||||
} catch {
|
||||
// WS is failing on 3.7 and bellow, use a fallback.
|
||||
this.courses = await CoreCoursesHelper.getUserCoursesWithOptions('lastaccess', 10, undefined, showCategories);
|
||||
|
||||
await this.loadContent(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the prefetch icon for selected courses.
|
||||
*/
|
||||
protected async initPrefetchCoursesIcons(): Promise<void> {
|
||||
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
|
||||
// Already initialized.
|
||||
return;
|
||||
}
|
||||
|
||||
this.prefetchIconsInitialized = true;
|
||||
const courseIds = recentCourses.map((course) => course.id);
|
||||
|
||||
this.prefetchCoursesData = await CoreCourseHelper.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData);
|
||||
// Get the courses using getCoursesByField to get more info about each course.
|
||||
const courses = await CoreCourses.getCoursesByField('ids', courseIds.join(','));
|
||||
|
||||
this.courses = recentCourses.map((recentCourse) => {
|
||||
const course = courses.find((course) => recentCourse.id == course.id);
|
||||
|
||||
return Object.assign(recentCourse, course);
|
||||
});
|
||||
|
||||
// Get course options and extra info.
|
||||
const options = await CoreCourses.getCoursesAdminAndNavOptions(courseIds);
|
||||
this.courses.forEach((course) => {
|
||||
course.navOptions = options.navOptions[course.id];
|
||||
course.admOptions = options.admOptions[course.id];
|
||||
|
||||
if (!showCategories) {
|
||||
course.categoryname = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event.
|
||||
* Refresh course list based on a EVENT_MY_COURSES_UPDATED event.
|
||||
*
|
||||
* @param data Event data.
|
||||
* @return Whether to refresh.
|
||||
*/
|
||||
protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean {
|
||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||
// Always update if user enrolled in a course.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.getCurrentSiteHomeId() &&
|
||||
this.courses[0] && data.courseId != this.courses[0].id) {
|
||||
// Update list if user viewed a course that isn't the most recent one and isn't site home.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && data.state == CoreCoursesProvider.STATE_FAVOURITE &&
|
||||
data.courseId && this.hasCourse(data.courseId)) {
|
||||
// Update list if a visible course is now favourite or unfavourite.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a certain course is in the list of courses.
|
||||
*
|
||||
* @param courseId Course ID to search.
|
||||
* @return Whether it's in the list.
|
||||
*/
|
||||
protected hasCourse(courseId: number): boolean {
|
||||
if (!this.courses) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!this.courses.find((course) => course.id == courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all the shown courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async prefetchCourses(): Promise<void> {
|
||||
const initialIcon = this.prefetchCoursesData.icon;
|
||||
protected async refreshCourseList(data: CoreCoursesMyCoursesUpdatedEventData): Promise<void> {
|
||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||
// Always update if user enrolled in a course.
|
||||
return await this.refreshContent();
|
||||
}
|
||||
|
||||
try {
|
||||
await CoreCourseHelper.prefetchCourses(this.courses, this.prefetchCoursesData);
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
this.prefetchCoursesData.icon = initialIcon;
|
||||
const courseIndex = this.courses.findIndex((course) => course.id == data.courseId);
|
||||
const course = this.courses[courseIndex];
|
||||
if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.getCurrentSiteHomeId()) {
|
||||
if (!course) {
|
||||
// Not found, use WS update.
|
||||
return await this.refreshContent();
|
||||
}
|
||||
|
||||
// Place at the begining.
|
||||
this.courses.splice(courseIndex, 1);
|
||||
this.courses.unshift(course);
|
||||
|
||||
await this.invalidateCourseList();
|
||||
}
|
||||
|
||||
if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED &&
|
||||
data.state == CoreCoursesProvider.STATE_FAVOURITE && course) {
|
||||
course.isfavourite = !!data.value;
|
||||
await this.invalidateCourseList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
this.coursesObserver?.off();
|
||||
this.updateSiteObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonBlockRecentlyAccessedCourse =
|
||||
(Omit<CoreCourseSummaryData, 'visible'> & CoreCourseSearchedDataWithExtraInfoAndOptions) |
|
||||
(CoreEnrolledCourseDataWithOptions & {
|
||||
categoryname?: string; // Category name,
|
||||
});
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
<ion-item-divider sticky="true">
|
||||
<ion-label><h2>{{ 'addon.block_recentlyaccesseditems.pluginname' | translate }}</h2></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.block_recentlyaccesseditems.pluginname' | translate }}</h2>
|
||||
</ion-label>
|
||||
<div slot="end">
|
||||
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
|
||||
</core-horizontal-scroll-controls>
|
||||
</div>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" [fullscreen]="false" class="safe-area-page">
|
||||
<div
|
||||
[id]="scrollElementId"
|
||||
[hidden]="!items || items.length === 0"
|
||||
class="core-horizontal-scroll"
|
||||
(scroll)="scrollControls.updateScrollPosition()"
|
||||
>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<div [id]="scrollElementId" [hidden]="!items || items.length === 0" class="core-horizontal-scroll"
|
||||
(scroll)="scrollControls.updateScrollPosition()">
|
||||
<div *ngIf="items" (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
|
||||
<div *ngFor="let item of items">
|
||||
<div class="safe-area-pseudo-padding-start"></div>
|
||||
<div *ngFor="let item of items" class="core-horizontal-scroll-item">
|
||||
<ion-card>
|
||||
<ion-item class="core-course-module-handler item-media ion-text-wrap" detail="false" (click)="action($event, item)"
|
||||
button>
|
||||
<img slot="start" [src]="item.iconUrl" alt="" role="presentation" *ngIf="item.iconUrl" class="core-module-icon">
|
||||
<ion-item class="core-course-module-handler ion-text-wrap" detail="false" (click)="action($event, item)" button>
|
||||
<core-mod-icon slot="start" *ngIf="item.iconUrl" [modicon]="item.iconUrl" [modname]="item.modname"
|
||||
[componentId]="item.cmid" [showAlt]="false">
|
||||
</core-mod-icon>
|
||||
<ion-label>
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
<span class="sr-only" *ngIf="item.iconTitle">{{ item.iconTitle }}</span>
|
||||
|
@ -33,10 +33,11 @@
|
|||
</ion-item>
|
||||
</ion-card>
|
||||
</div>
|
||||
<div class="safe-area-pseudo-padding-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<core-empty-box *ngIf="items.length <= 0" image="assets/img/icons/activities.svg" inline="true"
|
||||
<core-empty-box *ngIf="items.length <= 0" image="assets/img/icons/activities.svg"
|
||||
[message]="'addon.block_recentlyaccesseditems.noitems' | translate"></core-empty-box>
|
||||
|
||||
</core-loading>
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
.core-horizontal-scroll > div > div {
|
||||
.core-horizontal-scroll div.core-horizontal-scroll-item {
|
||||
@include horizontal_scroll_item(80%, 250px, 300px);
|
||||
ion-card {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.ion-text-wrap ion-label {
|
||||
.item-heading, h2, p {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-course-module-handler {
|
||||
--inner-border-width: 0;
|
||||
--inner-border-width: 0px;
|
||||
}
|
||||
core-loading {
|
||||
--loading-inline-min-height: 102px;
|
||||
|
|
|
@ -49,17 +49,41 @@ export class AddonBlockRecentlyAccessedItemsProvider {
|
|||
cacheKey: this.getRecentItemsCacheKey(),
|
||||
};
|
||||
|
||||
const items: AddonBlockRecentlyAccessedItemsItem[] =
|
||||
let items: AddonBlockRecentlyAccessedItemsItem[] =
|
||||
await site.read('block_recentlyaccesseditems_get_recent_items', undefined, preSets);
|
||||
|
||||
return items.map((item) => {
|
||||
const cmIds: number[] = [];
|
||||
|
||||
items = await Promise.all(items.map(async (item) => {
|
||||
const modicon = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'src');
|
||||
|
||||
item.iconUrl = CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
|
||||
item.iconUrl = await CoreCourse.getModuleIconSrc(item.modname, modicon || undefined);
|
||||
item.iconTitle = item.icon && CoreDomUtils.getHTMLElementAttribute(item.icon, 'title');
|
||||
cmIds.push(item.cmid);
|
||||
|
||||
return item;
|
||||
}));
|
||||
|
||||
// Check if the viewed module should be updated for each activity.
|
||||
const lastViewedMap = await CoreCourse.getCertainModulesViewed(cmIds, site.getId());
|
||||
|
||||
items.forEach((recentItem) => {
|
||||
const timeAccess = recentItem.timeaccess * 1000;
|
||||
const lastViewed = lastViewedMap[recentItem.cmid];
|
||||
|
||||
if (lastViewed && lastViewed.timeaccess >= timeAccess) {
|
||||
return; // No need to update.
|
||||
}
|
||||
|
||||
// Update access.
|
||||
CoreCourse.storeModuleViewed(recentItem.courseid, recentItem.cmid, {
|
||||
timeaccess: recentItem.timeaccess * 1000,
|
||||
sectionId: lastViewed && lastViewed.sectionId,
|
||||
siteId: site.getId(),
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
-webkit-padding-start: 0;
|
||||
|
||||
li {
|
||||
border-top: 1px solid var(--gray);
|
||||
border-top: 1px solid var(--stroke);
|
||||
padding: 5px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
<ion-item-divider sticky="true">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.block_sitemainmenu.pluginname' | translate }}</h2>
|
||||
<h2>{{ 'addon.block_sitemainmenu.pluginname' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" [fullscreen]="false" class="margin">
|
||||
<ng-container *ngIf="mainMenuBlock">
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-list *ngIf="mainMenuBlock" class="core-course-module-list-wrapper">
|
||||
<ion-item class="ion-text-wrap" *ngIf="mainMenuBlock.summary">
|
||||
<ion-label>
|
||||
<core-format-text [text]="mainMenuBlock.summary" [component]="component" [componentId]="siteHomeId"
|
||||
contextLevel="course" [contextInstanceId]="siteHomeId"></core-format-text>
|
||||
<core-format-text [text]="mainMenuBlock.summary" [component]="component" [componentId]="siteHomeId" contextLevel="course"
|
||||
[contextInstanceId]="siteHomeId"></core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [courseId]="siteHomeId"
|
||||
[downloadEnabled]="downloadEnabled" [section]="mainMenuBlock"></core-course-module>
|
||||
</ng-container>
|
||||
<core-course-module *ngFor="let module of mainMenuBlock.modules" [module]="module" [section]="mainMenuBlock"></core-course-module>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
|
@ -29,8 +29,6 @@ import { CoreBlockBaseComponent } from '@features/block/classes/base-block-compo
|
|||
})
|
||||
export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent implements OnInit {
|
||||
|
||||
@Input() downloadEnabled = false;
|
||||
|
||||
component = 'AddonBlockSiteMainMenu';
|
||||
mainMenuBlock?: CoreCourseSection;
|
||||
siteHomeId = 1;
|
||||
|
@ -91,7 +89,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
|||
const items = config.frontpageloggedin.split(',');
|
||||
const hasNewsItem = items.find((item) => parseInt(item, 10) == FrontPageItemNames['NEWS_ITEMS']);
|
||||
|
||||
const result = CoreCourseHelper.addHandlerDataForModules(
|
||||
const result = await CoreCourseHelper.addHandlerDataForModules(
|
||||
[mainMenuBlock],
|
||||
this.siteHomeId,
|
||||
undefined,
|
||||
|
|
|
@ -1,41 +1,25 @@
|
|||
<ion-item-divider sticky="true">
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.block_starredcourses.pluginname' | translate }}</h2>
|
||||
<h2>{{ 'addon.block_starredcourses.pluginname' | translate }}</h2>
|
||||
</ion-label>
|
||||
<div slot="end" class="flex-row">
|
||||
<div *ngIf="downloadCoursesEnabled && downloadEnabled && courses && courses.length > 1" class="core-button-spinner">
|
||||
<ion-button *ngIf="prefetchCoursesData.icon && !prefetchCoursesData.loading" fill="clear" color="dark"
|
||||
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
|
||||
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge"
|
||||
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData.total"
|
||||
[attr.aria-valuenow]="prefetchCoursesData.count" [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText">
|
||||
{{prefetchCoursesData.badge}}
|
||||
</ion-badge>
|
||||
<ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading"
|
||||
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
</div>
|
||||
|
||||
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
|
||||
</core-horizontal-scroll-controls>
|
||||
</div>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" [fullscreen]="false" class="safe-area-page margin">
|
||||
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg"
|
||||
[message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box>
|
||||
<!-- List of courses. -->
|
||||
<div
|
||||
[hidden]="courses.length === 0"
|
||||
[id]="scrollElementId"
|
||||
class="core-horizontal-scroll"
|
||||
(scroll)="scrollControls.updateScrollPosition()"
|
||||
>
|
||||
<div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll"
|
||||
(scroll)="scrollControls.updateScrollPosition()">
|
||||
<div (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
|
||||
<div class="safe-area-pseudo-padding-start"></div>
|
||||
<ng-container *ngFor="let course of courses">
|
||||
<core-courses-course-progress [course]="course" class="core-block_starredcourses"
|
||||
[showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress>
|
||||
<core-courses-course-list-item [course]="course" class="core-block_starredcourses" layout="summarycard">
|
||||
</core-courses-course-list-item>
|
||||
</ng-container>
|
||||
<div class="safe-area-pseudo-padding-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
</core-loading>
|
||||
|
|
|
@ -12,17 +12,20 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
|
||||
import {
|
||||
CoreCourseSearchedDataWithExtraInfoAndOptions,
|
||||
CoreEnrolledCourseDataWithOptions,
|
||||
} from '@features/courses/services/courses-helper';
|
||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||
import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion';
|
||||
import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion';
|
||||
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreSite } from '@classes/site';
|
||||
import { AddonBlockStarredCourse, AddonBlockStarredCourses } from '../../services/starredcourses';
|
||||
|
||||
/**
|
||||
* Component to render a starred courses block.
|
||||
|
@ -31,36 +34,25 @@ import { CoreDomUtils } from '@services/utils/dom';
|
|||
selector: 'addon-block-starredcourses',
|
||||
templateUrl: 'addon-block-starredcourses.html',
|
||||
})
|
||||
export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
|
||||
export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() downloadEnabled = false;
|
||||
courses: AddonBlockStarredCoursesCourse[] = [];
|
||||
|
||||
courses: CoreEnrolledCourseDataWithOptions [] = [];
|
||||
prefetchCoursesData: CorePrefetchStatusInfo = {
|
||||
icon: '',
|
||||
statusTranslatable: 'core.loading',
|
||||
status: '',
|
||||
loading: true,
|
||||
badge: '',
|
||||
};
|
||||
|
||||
downloadCourseEnabled = false;
|
||||
downloadCoursesEnabled = false;
|
||||
scrollElementId!: string;
|
||||
|
||||
protected prefetchIconsInitialized = false;
|
||||
protected site: CoreSite;
|
||||
protected isDestroyed = false;
|
||||
protected coursesObserver?: CoreEventObserver;
|
||||
protected updateSiteObserver?: CoreEventObserver;
|
||||
protected courseIds: number[] = [];
|
||||
protected fetchContentDefaultError = 'Error getting starred courses data.';
|
||||
|
||||
constructor() {
|
||||
super('AddonBlockStarredCoursesComponent');
|
||||
|
||||
this.site = CoreSites.getRequiredCurrentSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Generate unique id for scroll element.
|
||||
|
@ -68,25 +60,10 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
|
|||
|
||||
this.scrollElementId = `addon-block-starredcourses-scroll-${scrollId}`;
|
||||
|
||||
// Refresh the enabled flags if enabled.
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
|
||||
|
||||
}, CoreSites.getCurrentSiteId());
|
||||
|
||||
this.coursesObserver = CoreEvents.on(
|
||||
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
|
||||
(data) => {
|
||||
|
||||
if (this.shouldRefreshOnUpdatedEvent(data)) {
|
||||
this.refreshCourseList();
|
||||
}
|
||||
this.refreshContent();
|
||||
this.refreshCourseList(data);
|
||||
},
|
||||
|
||||
CoreSites.getCurrentSiteId(),
|
||||
|
@ -96,128 +73,130 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
|
|||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||
if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
|
||||
// Download all courses is enabled now, initialize it.
|
||||
this.initPrefetchCoursesIcons();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the invalidate content function.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async invalidateContent(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
const courseIds = this.courses.map((course) => course.id);
|
||||
|
||||
promises.push(CoreCourses.invalidateUserCourses().finally(() =>
|
||||
// Invalidate course completion data.
|
||||
CoreUtils.allPromises(this.courseIds.map((courseId) =>
|
||||
AddonCourseCompletion.invalidateCourseCompletion(courseId)))));
|
||||
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||
if (this.courseIds.length > 0) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds.join(',')));
|
||||
}
|
||||
|
||||
await CoreUtils.allPromises(promises).finally(() => {
|
||||
this.prefetchIconsInitialized = false;
|
||||
});
|
||||
await this.invalidateCourses(courseIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the courses.
|
||||
* Invalidate list of courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async invalidateCourseList(): Promise<void> {
|
||||
return AddonBlockStarredCourses.invalidateStarredCourses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to invalidate only selected courses.
|
||||
*
|
||||
* @param courseIds Course Id array.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async invalidateCourses(courseIds: number[]): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// Invalidate course completion data.
|
||||
promises.push(this.invalidateCourseList().finally(() =>
|
||||
CoreUtils.allPromises(courseIds.map((courseId) =>
|
||||
AddonCourseCompletion.invalidateCourseCompletion(courseId)))));
|
||||
|
||||
if (courseIds.length == 1) {
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(courseIds[0]));
|
||||
} else {
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||
}
|
||||
if (courseIds.length > 0) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('ids', courseIds.join(',')));
|
||||
}
|
||||
|
||||
await CoreUtils.allPromises(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected async fetchContent(): Promise<void> {
|
||||
const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories &&
|
||||
this.block.configsRecord.displaycategories.value == '1';
|
||||
|
||||
this.courses = await CoreCoursesHelper.getUserCoursesWithOptions('timemodified', 0, 'isfavourite', showCategories);
|
||||
this.initPrefetchCoursesIcons();
|
||||
// Timemodified not present, use the block WS to retrieve the info.
|
||||
const starredCourses = await AddonBlockStarredCourses.getStarredCourses();
|
||||
|
||||
const courseIds = starredCourses.map((course) => course.id);
|
||||
|
||||
// Get the courses using getCoursesByField to get more info about each course.
|
||||
const courses = await CoreCourses.getCoursesByField('ids', courseIds.join(','));
|
||||
|
||||
this.courses = starredCourses.map((recentCourse) => {
|
||||
const course = courses.find((course) => recentCourse.id == course.id);
|
||||
|
||||
return Object.assign(recentCourse, course);
|
||||
});
|
||||
|
||||
// Get course options and extra info.
|
||||
const options = await CoreCourses.getCoursesAdminAndNavOptions(courseIds);
|
||||
this.courses.forEach((course) => {
|
||||
course.navOptions = options.navOptions[course.id];
|
||||
course.admOptions = options.admOptions[course.id];
|
||||
|
||||
if (!showCategories) {
|
||||
course.categoryname = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the list of courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async refreshCourseList(): Promise<void> {
|
||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED);
|
||||
|
||||
try {
|
||||
await CoreCourses.invalidateUserCourses();
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
await this.loadContent(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event.
|
||||
* Refresh course list based on a EVENT_MY_COURSES_UPDATED event.
|
||||
*
|
||||
* @param data Event data.
|
||||
* @return Whether to refresh.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean {
|
||||
protected async refreshCourseList(data: CoreCoursesMyCoursesUpdatedEventData): Promise<void> {
|
||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||
// Always update if user enrolled in a course.
|
||||
// New courses shouldn't be favourite by default, but just in case.
|
||||
return true;
|
||||
return await this.refreshContent();
|
||||
}
|
||||
|
||||
if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && data.state == CoreCoursesProvider.STATE_FAVOURITE) {
|
||||
// Update list when making a course favourite or not.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the prefetch icon for selected courses.
|
||||
*/
|
||||
protected async initPrefetchCoursesIcons(): Promise<void> {
|
||||
if (this.prefetchIconsInitialized || !this.downloadEnabled) {
|
||||
// Already initialized.
|
||||
return;
|
||||
}
|
||||
|
||||
this.prefetchIconsInitialized = true;
|
||||
|
||||
this.prefetchCoursesData = await CoreCourseHelper.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all the shown courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async prefetchCourses(): Promise<void> {
|
||||
const initialIcon = this.prefetchCoursesData.icon;
|
||||
|
||||
try {
|
||||
return CoreCourseHelper.prefetchCourses(this.courses, this.prefetchCoursesData);
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
this.prefetchCoursesData.icon = initialIcon;
|
||||
const courseIndex = this.courses.findIndex((course) => course.id == data.courseId);
|
||||
if (courseIndex < 0) {
|
||||
// Not found, use WS update. Usually new favourite.
|
||||
return await this.refreshContent();
|
||||
}
|
||||
|
||||
const course = this.courses[courseIndex];
|
||||
if (data.value === false) {
|
||||
// Unfavourite, just remove.
|
||||
this.courses.splice(courseIndex, 1);
|
||||
} else {
|
||||
// List is not synced, favourite course and place it at the begining.
|
||||
course.isfavourite = !!data.value;
|
||||
|
||||
this.courses.splice(courseIndex, 1);
|
||||
this.courses.unshift(course);
|
||||
}
|
||||
|
||||
await this.invalidateCourseList();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
this.coursesObserver?.off();
|
||||
this.updateSiteObserver?.off();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonBlockStarredCoursesCourse =
|
||||
(AddonBlockStarredCourse & CoreCourseSearchedDataWithExtraInfoAndOptions) |
|
||||
(CoreEnrolledCourseDataWithOptions & {
|
||||
categoryname?: string; // Category name,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSiteWSPreSets } from '@classes/site';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
||||
const ROOT_CACHE_KEY = 'AddonBlockStarredCourses:';
|
||||
|
||||
/**
|
||||
* Service that provides some features regarding starred courses.
|
||||
*/
|
||||
@Injectable( { providedIn: 'root' })
|
||||
export class AddonBlockStarredCoursesProvider {
|
||||
|
||||
/**
|
||||
* Get cache key for get starred courrses value WS call.
|
||||
*
|
||||
* @return Cache key.
|
||||
*/
|
||||
protected getStarredCoursesCacheKey(): string {
|
||||
return ROOT_CACHE_KEY + ':starredcourses';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get starred courrses.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
async getStarredCourses(siteId?: string): Promise<AddonBlockStarredCourse[]> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
cacheKey: this.getStarredCoursesCacheKey(),
|
||||
};
|
||||
|
||||
return await site.read<AddonBlockStarredCourse[]>('block_starredcourses_get_starred_courses', undefined, preSets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates get starred courrses WS call.
|
||||
*
|
||||
* @param siteId Site ID to invalidate. If not defined, use current site.
|
||||
* @return Promise resolved when the data is invalidated.
|
||||
*/
|
||||
async invalidateStarredCourses(siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
await site.invalidateWsCacheForKey(this.getStarredCoursesCacheKey());
|
||||
}
|
||||
|
||||
}
|
||||
export const AddonBlockStarredCourses = makeSingleton(AddonBlockStarredCoursesProvider);
|
||||
|
||||
/**
|
||||
* Params of block_starredcourses_get_starred_courses WS.
|
||||
*/
|
||||
export type AddonBlockStarredCoursesGetStarredCoursesWSParams = {
|
||||
limit?: number; // Limit.
|
||||
offset?: number; // Offset.
|
||||
};
|
||||
|
||||
/**
|
||||
* Data returned by block_starredcourses_get_starred_courses WS.
|
||||
*/
|
||||
export type AddonBlockStarredCourse = {
|
||||
id: number; // Id.
|
||||
fullname: string; // Fullname.
|
||||
shortname: string; // Shortname.
|
||||
idnumber: string; // Idnumber.
|
||||
summary: string; // Summary.
|
||||
summaryformat: number; // Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||
startdate: number; // Startdate.
|
||||
enddate: number; // Enddate.
|
||||
visible: boolean; // Visible.
|
||||
showactivitydates: boolean; // Showactivitydates.
|
||||
showcompletionconditions: boolean; // Showcompletionconditions.
|
||||
fullnamedisplay: string; // Fullnamedisplay.
|
||||
viewurl: string; // Viewurl.
|
||||
courseimage: string; // Courseimage.
|
||||
progress?: number; // Progress.
|
||||
hasprogress: boolean; // Hasprogress.
|
||||
isfavourite: boolean; // Isfavourite.
|
||||
hidden: boolean; // Hidden.
|
||||
timeaccess?: number; // Timeaccess.
|
||||
showshortname: boolean; // Showshortname.
|
||||
coursecategory: string; // Coursecategory.
|
||||
};
|
|
@ -1,11 +1,9 @@
|
|||
:host .core-block-content ::ng-deep {
|
||||
.tag_cloud {
|
||||
font-size: 80%;
|
||||
text-align: center;
|
||||
ul.inline-list {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin: 0;
|
||||
-webkit-padding-start: 0;
|
||||
|
||||
li {
|
||||
|
@ -13,8 +11,8 @@
|
|||
display: inline-block;
|
||||
|
||||
a {
|
||||
background: var(--ion-color-primary);
|
||||
color: var(--ion-color-primary-contrast);
|
||||
background: var(--primary);
|
||||
color: var(--primary-contrast);
|
||||
padding: 3px 8px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
|
@ -26,7 +24,7 @@
|
|||
contain: content;
|
||||
vertical-align: baseline;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--small-radius);
|
||||
}
|
||||
.s20 {
|
||||
font-size: 2.7em;
|
||||
|
|
|
@ -15,11 +15,10 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { CoreSharedModule } from '@/core/shared.module';
|
||||
import { CoreCoursesComponentsModule } from '@features/courses/components/components.module';
|
||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||
|
||||
import { AddonBlockTimelineComponent } from './timeline/timeline';
|
||||
import { AddonBlockTimelineEventsComponent } from './events/events';
|
||||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -28,8 +27,7 @@ import { AddonBlockTimelineEventsComponent } from './events/events';
|
|||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
CoreCoursesComponentsModule,
|
||||
CoreCourseComponentsModule,
|
||||
CoreSearchComponentsModule,
|
||||
],
|
||||
exports: [
|
||||
AddonBlockTimelineComponent,
|
||||
|
|
|
@ -1,62 +1,83 @@
|
|||
<ion-item *ngIf="course">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<h3>
|
||||
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
|
||||
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</h3>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-group *ngFor="let dayEvents of filteredEvents">
|
||||
<ion-item-divider [color]="dayEvents.color">
|
||||
<ion-label><h3>{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedayshort" }}</h3></ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h4 [class.core-bold]="!course">{{ dayEvents.dayTimestamp * 1000 | coreFormatDate:"strftimedaydate" }}</h4>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ng-container *ngFor="let event of dayEvents.events">
|
||||
<ion-item class="ion-text-wrap core-course-module-handler item-media" detail="false" (click)="action($event, event.url)"
|
||||
[attr.aria-label]="event.name" button>
|
||||
<img slot="start" [src]="event.iconUrl" alt="" role="presentation" *ngIf="event.iconUrl" class="core-module-icon">
|
||||
<ion-item class="addon-block-timeline-activity" detail="false" (click)="action($event, event.url)" [attr.aria-label]="event.name"
|
||||
button lines="full">
|
||||
<ion-label>
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
<span class="sr-only" *ngIf="event.iconTitle">{{ event.iconTitle }}</span>
|
||||
<p class="item-heading">
|
||||
<core-format-text [text]="event.name" contextLevel="module" [contextInstanceId]="event.id"
|
||||
[courseId]="event.course && event.course.id">
|
||||
</core-format-text>
|
||||
</p>
|
||||
<p *ngIf="showCourse && event.course">
|
||||
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
|
||||
[contextInstanceId]="event.course.id">
|
||||
</core-format-text>
|
||||
</p>
|
||||
|
||||
<ion-button fill="clear" class="ion-hide-md-up ion-text-wrap" (click)="action($event, event.action.url)"
|
||||
[title]="event.action.name" [disabled]="!event.action.actionable" *ngIf="event.action">
|
||||
{{event.action.name}}
|
||||
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">{{event.action.itemcount}}
|
||||
</ion-badge>
|
||||
</ion-button>
|
||||
<ion-row class="ion-justify-content-between ion-align-items-center ion-no-padding">
|
||||
<ion-col class="addon-block-timeline-activity-main ion-no-padding">
|
||||
<ion-row class="ion-justify-content-between ion-align-items-center ion-nowrap ion-no-padding">
|
||||
<ion-col class="addon-block-timeline-activity-time ion-no-padding">
|
||||
<small>{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</small>
|
||||
<core-mod-icon *ngIf="event.iconUrl" [modicon]="event.iconUrl" [componentId]="event.instance"
|
||||
[modname]="event.modulename">
|
||||
</core-mod-icon>
|
||||
</ion-col>
|
||||
<ion-col class="addon-block-timeline-activity-name ion-no-padding">
|
||||
<p class="item-heading addon-block-timeline-activity-name-with-status">
|
||||
<span>
|
||||
<core-format-text [text]="event.activityname || event.name" contextLevel="module"
|
||||
[contextInstanceId]="event.id" [courseId]="event.course && event.course.id">
|
||||
</core-format-text>
|
||||
</span>
|
||||
<ion-badge *ngIf="event.overdue" color="danger">{{ 'addon.block_timeline.overdue' | translate }}
|
||||
</ion-badge>
|
||||
</p>
|
||||
<p *ngIf="(showCourse && event.course) || event.activitystr"
|
||||
class="addon-block-timeline-activity-course-activity">
|
||||
<span *ngIf="showCourse && event.course">
|
||||
<core-format-text [text]="event.course.fullnamedisplay" contextLevel="course"
|
||||
[contextInstanceId]="event.course.id">
|
||||
</core-format-text>
|
||||
</span>
|
||||
<span *ngIf="event.activitystr">
|
||||
<core-format-text *ngIf="event.activitystr" [text]="event.activitystr" contextLevel="module"
|
||||
[contextInstanceId]="event.id">
|
||||
</core-format-text>
|
||||
</span>
|
||||
</p>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-col>
|
||||
<ion-col class="addon-block-timeline-activity-action ion-no-padding" *ngIf="event.action?.actionable">
|
||||
<ion-button fill="outline" (click)="action($event, event.action.url)" [title]="event.action.name" class="chip">
|
||||
{{event.action.name}}
|
||||
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">
|
||||
{{event.action.itemcount}}
|
||||
</ion-badge>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-label>
|
||||
|
||||
<div slot="end" class="events-info">
|
||||
<div>
|
||||
<ion-badge color="light">{{event.timesort * 1000 | coreFormatDate:"strftimetime24" }}</ion-badge>
|
||||
</div>
|
||||
<ion-button
|
||||
class="ion-hide-md-down"
|
||||
fill="clear"
|
||||
(click)="action($event, event.action.url)"
|
||||
[title]="event.action.name"
|
||||
[disabled]="!event.action.actionable" *ngIf="event.action"
|
||||
>
|
||||
{{event.action.name}}
|
||||
<ion-badge slot="end" class="ion-margin-start" *ngIf="event.action.showitemcount">
|
||||
{{event.action.itemcount}}
|
||||
</ion-badge>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
|
||||
<div class="ion-padding ion-text-center" *ngIf="canLoadMore && !empty">
|
||||
<!-- Button and spinner to show more attempts. -->
|
||||
<ion-button expand="block" (click)="loadMoreEvents()" color="light" *ngIf="!loadingMore">
|
||||
<ion-button expand="block" (click)="loadMoreEvents()" fill="outline" *ngIf="!loadingMore">
|
||||
{{ 'core.loadmore' | translate }}
|
||||
</ion-button>
|
||||
<ion-spinner *ngIf="loadingMore" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
</div>
|
||||
|
||||
<core-empty-box *ngIf="empty" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate"
|
||||
inline="true">
|
||||
<ion-item *ngIf="empty && course">
|
||||
<ion-label class="ion-text-wrap">
|
||||
<p>{{'addon.block_timeline.noevents' | translate}}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<core-empty-box *ngIf="empty && !course" image="assets/img/icons/activities.svg" [message]="'addon.block_timeline.noevents' | translate">
|
||||
</core-empty-box>
|
||||
|
|
|
@ -1,6 +1,81 @@
|
|||
.events-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: end;
|
||||
padding: 10px 0;
|
||||
@import "~theme/globals";
|
||||
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
h4.core-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.addon-block-timeline-activity {
|
||||
ion-badge {
|
||||
@include margin-horizontal(0.25rem, 0.5rem);
|
||||
}
|
||||
|
||||
small {
|
||||
@include margin-horizontal(null, 0.5rem);
|
||||
}
|
||||
|
||||
core-mod-icon {
|
||||
padding: 8px;
|
||||
--margin-end: 0.5rem;
|
||||
--margin-vertical: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-block-timeline-activity-time {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.addon-block-timeline-activity-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.addon-block-timeline-activity-name-with-status {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-block-timeline-activity-course-activity {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
span::after {
|
||||
content: "·";
|
||||
display: inline;
|
||||
padding-left: .3rem;
|
||||
padding-right: .3rem;
|
||||
}
|
||||
|
||||
span:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-block-timeline-activity-main,
|
||||
.addon-block-timeline-activity-name {
|
||||
flex-grow: 1;
|
||||
p {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-block-timeline-activity-name {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import moment from 'moment';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
|
||||
import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
|
||||
import { CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper';
|
||||
import { AddonBlockTimeline } from '../../services/timeline';
|
||||
|
||||
/**
|
||||
* Directive to render a list of events in course overview.
|
||||
|
@ -34,34 +34,33 @@ import { AddonCalendarEvent } from '@addons/calendar/services/calendar';
|
|||
export class AddonBlockTimelineEventsComponent implements OnChanges {
|
||||
|
||||
@Input() events: AddonBlockTimelineEvent[] = []; // The events to render.
|
||||
@Input() showCourse?: boolean | string; // Whether to show the course name.
|
||||
@Input() course?: CoreEnrolledCourseDataWithOptions; // Whether to show the course name.
|
||||
@Input() from = 0; // Number of days from today to offset the events.
|
||||
@Input() to?: number; // Number of days from today to limit the events to. If not defined, no limit.
|
||||
@Input() canLoadMore?: boolean; // Whether more events can be loaded.
|
||||
@Output() loadMore: EventEmitter<void>; // Notify that more events should be loaded.
|
||||
@Input() overdue = false; // If filtering overdue events or not.
|
||||
@Input() canLoadMore = false; // Whether more events can be loaded.
|
||||
@Output() loadMore = new EventEmitter(); // Notify that more events should be loaded.
|
||||
|
||||
showCourse = false; // Whether to show the course name.
|
||||
empty = true;
|
||||
loadingMore = false;
|
||||
filteredEvents: AddonBlockTimelineEventFilteredEvent[] = [];
|
||||
|
||||
constructor() {
|
||||
this.loadMore = new EventEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||
this.showCourse = CoreUtils.isTrueOrOne(this.showCourse);
|
||||
async ngOnChanges(changes: {[name: string]: SimpleChange}): Promise<void> {
|
||||
this.showCourse = !this.course;
|
||||
|
||||
if (changes.events || changes.from || changes.to) {
|
||||
if (this.events && this.events.length > 0) {
|
||||
const filteredEvents = this.filterEventsByTime(this.from, this.to);
|
||||
if (this.events) {
|
||||
const filteredEvents = await this.filterEventsByTime();
|
||||
this.empty = !filteredEvents || filteredEvents.length <= 0;
|
||||
|
||||
const eventsByDay: Record<number, AddonCalendarEvent[]> = {};
|
||||
const eventsByDay: Record<number, AddonBlockTimelineEvent[]> = {};
|
||||
filteredEvents.forEach((event) => {
|
||||
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);
|
||||
|
||||
if (eventsByDay[dayTimestamp]) {
|
||||
eventsByDay[dayTimestamp].push(event);
|
||||
} else {
|
||||
|
@ -69,16 +68,15 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
|||
}
|
||||
});
|
||||
|
||||
const todaysMidnight = CoreTimeUtils.getMidnightForTimestamp();
|
||||
this.filteredEvents = [];
|
||||
Object.keys(eventsByDay).forEach((key) => {
|
||||
this.filteredEvents = Object.keys(eventsByDay).map((key) => {
|
||||
const dayTimestamp = parseInt(key);
|
||||
this.filteredEvents.push({
|
||||
color: dayTimestamp < todaysMidnight ? 'danger' : 'light',
|
||||
|
||||
return {
|
||||
dayTimestamp,
|
||||
events: eventsByDay[dayTimestamp],
|
||||
});
|
||||
};
|
||||
});
|
||||
this.loadingMore = false;
|
||||
} else {
|
||||
this.empty = true;
|
||||
}
|
||||
|
@ -88,26 +86,41 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
|||
/**
|
||||
* Filter the events by time.
|
||||
*
|
||||
* @param start Number of days to start getting events from today. E.g. -1 will get events from yesterday.
|
||||
* @param end Number of days after the start.
|
||||
* @return Filtered events.
|
||||
*/
|
||||
protected filterEventsByTime(start: number, end?: number): AddonBlockTimelineEvent[] {
|
||||
start = moment().add(start, 'days').startOf('day').unix();
|
||||
end = typeof end != 'undefined' ? moment().add(end, 'days').startOf('day').unix() : end;
|
||||
protected async filterEventsByTime(): Promise<AddonBlockTimelineEvent[]> {
|
||||
const start = AddonBlockTimeline.getDayStart(this.from);
|
||||
const end = this.to !== undefined
|
||||
? AddonBlockTimeline.getDayStart(this.to)
|
||||
: undefined;
|
||||
|
||||
return this.events.filter((event) => {
|
||||
if (end) {
|
||||
return start <= event.timesort && event.timesort < end;
|
||||
const now = CoreTimeUtils.timestamp();
|
||||
const midnight = AddonBlockTimeline.getDayStart();
|
||||
|
||||
return await Promise.all(this.events.filter((event) => {
|
||||
if (start > event.timesort || (end && event.timesort >= end)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return start <= event.timesort;
|
||||
}).map((event) => {
|
||||
event.iconUrl = CoreCourse.getModuleIconSrc(event.icon.component);
|
||||
event.iconTitle = event.modulename && CoreCourse.translateModuleName(event.modulename);
|
||||
// Already calculated on 4.0 onwards but this will be live.
|
||||
event.overdue = event.timesort < now;
|
||||
|
||||
if (event.eventtype === 'open' || event.eventtype === 'opensubmission') {
|
||||
const dayTimestamp = CoreTimeUtils.getMidnightForTimestamp(event.timesort);
|
||||
|
||||
return dayTimestamp > midnight;
|
||||
}
|
||||
|
||||
// When filtering by overdue, we fetch all events due today, in case any have elapsed already and are overdue.
|
||||
// This means if filtering by overdue, some events fetched might not be required (eg if due later today).
|
||||
return (!this.overdue || event.overdue);
|
||||
}).map(async (event) => {
|
||||
event.iconUrl = await CoreCourse.getModuleIconSrc(event.icon.component);
|
||||
event.modulename = event.modulename || event.icon.component;
|
||||
event.iconTitle = CoreCourse.translateModuleName(event.modulename);
|
||||
|
||||
return event;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,12 +134,12 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
|||
/**
|
||||
* Action clicked.
|
||||
*
|
||||
* @param e Click event.
|
||||
* @param event Click event.
|
||||
* @param url Url of the action.
|
||||
*/
|
||||
async action(e: Event, url: string): Promise<void> {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
async action(event: Event, url: string): Promise<void> {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Fix URL format.
|
||||
url = CoreTextUtils.decodeHTMLEntities(url);
|
||||
|
@ -136,7 +149,7 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
|||
try {
|
||||
const treated = await CoreContentLinksHelper.handleLink(url);
|
||||
if (!treated) {
|
||||
return CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(url);
|
||||
return CoreSites.getRequiredCurrentSite().openInBrowserWithAutoLoginIfSameSite(url);
|
||||
}
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
|
@ -145,7 +158,8 @@ export class AddonBlockTimelineEventsComponent implements OnChanges {
|
|||
|
||||
}
|
||||
|
||||
type AddonBlockTimelineEvent = AddonCalendarEvent & {
|
||||
type AddonBlockTimelineEvent = Omit<AddonCalendarEvent, 'eventtype'> & {
|
||||
eventtype: string;
|
||||
iconUrl?: string;
|
||||
iconTitle?: string;
|
||||
};
|
||||
|
@ -153,5 +167,4 @@ type AddonBlockTimelineEvent = AddonCalendarEvent & {
|
|||
type AddonBlockTimelineEventFilteredEvent = {
|
||||
events: AddonBlockTimelineEvent[];
|
||||
dayTimestamp: number;
|
||||
color: string;
|
||||
};
|
||||
|
|
|
@ -1,57 +1,73 @@
|
|||
<ion-item-divider sticky="true">
|
||||
<ion-label><h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2></ion-label>
|
||||
<core-context-menu slot="end">
|
||||
<core-context-menu-item *ngIf="loaded" [priority]="900" [content]="'addon.block_timeline.sortbydates' | translate"
|
||||
(action)="switchSort('sortbydates')" [iconAction]="sort == 'sortbydates' ? 'far-dot-circle' : 'far-circle'">
|
||||
</core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="loaded" [priority]="800" [content]="'addon.block_timeline.sortbycourses' | translate"
|
||||
(action)="switchSort('sortbycourses')" [iconAction]="sort == 'sortbycourses' ? 'far-dot-circle' : 'far-circle'">
|
||||
</core-context-menu-item>
|
||||
</core-context-menu>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.block_timeline.pluginname' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<core-loading [hideUntil]="loaded" [fullscreen]="false" class="margin">
|
||||
<div class="safe-padding-horizontal">
|
||||
<core-combobox [selection]="filter" (onChange)="switchFilter($event)">
|
||||
<ion-select-option class="ion-text-wrap" value="all">
|
||||
{{ 'core.all' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="overdue">
|
||||
{{ 'addon.block_timeline.overdue' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" disabled value="disabled">
|
||||
{{ 'addon.block_timeline.duedate' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="next7days">
|
||||
{{ 'addon.block_timeline.next7days' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="next30days">
|
||||
{{ 'addon.block_timeline.next30days' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="next3months">
|
||||
{{ 'addon.block_timeline.next3months' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="next6months">
|
||||
{{ 'addon.block_timeline.next6months' | translate }}
|
||||
</ion-select-option>
|
||||
</core-combobox>
|
||||
</div>
|
||||
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'" [fullscreen]="false" class="margin">
|
||||
<addon-block-timeline-events [events]="timeline.events" showCourse="true" [canLoadMore]="timeline.canLoadMore"
|
||||
(loadMore)="loadMoreTimeline()" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-row class="ion-hide-md-up addon-block-timeline-filter" *ngIf="searchEnabled">
|
||||
<ion-col>
|
||||
<!-- Filter courses. -->
|
||||
<core-search-box (onSubmit)="searchTextChanged($event)" (onClear)="searchTextChanged()"
|
||||
[placeholder]="'addon.block_timeline.searchevents' | translate" autocorrect="off" spellcheck="false" lengthCheck="2"
|
||||
searchArea="AddonBlockTimeline"></core-search-box>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
<ion-row class="ion-justify-content-between ion-align-items-center addon-block-timeline-filter">
|
||||
<ion-col size="auto">
|
||||
<core-combobox [selection]="filter" (onChange)="switchFilter($event)">
|
||||
<ion-select-option class="ion-text-wrap" value="all">
|
||||
{{ 'core.all' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap core-select-option-border-bottom" value="overdue">
|
||||
{{ 'addon.block_timeline.overdue' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap core-select-option-title" disabled value="disabled">
|
||||
{{ 'addon.block_timeline.duedate' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="next7days">
|
||||
{{ 'addon.block_timeline.next7days' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="next30days">
|
||||
{{ 'addon.block_timeline.next30days' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="next3months">
|
||||
{{ 'addon.block_timeline.next3months' | translate }}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="next6months">
|
||||
{{ 'addon.block_timeline.next6months' | translate }}
|
||||
</ion-select-option>
|
||||
</core-combobox>
|
||||
</ion-col>
|
||||
<ion-col class="ion-hide-md-down" *ngIf="searchEnabled">
|
||||
<!-- Filter courses. -->
|
||||
<core-search-box (onSubmit)="searchTextChanged($event)" (onClear)="searchTextChanged()"
|
||||
[placeholder]="'addon.block_timeline.searchevents' | translate" autocorrect="off" spellcheck="false" lengthCheck="2"
|
||||
searchArea="AddonBlockTimeline"></core-search-box>
|
||||
</ion-col>
|
||||
<ion-col size="auto">
|
||||
<core-combobox [label]="'core.sortby' | translate" [selection]="sort" (onChange)="switchSort($event)"
|
||||
icon="fas-sort-amount-down-alt">
|
||||
<ion-select-option class="ion-text-wrap" value="sortbydates">
|
||||
{{'addon.block_timeline.sortbydates' | translate}}
|
||||
</ion-select-option>
|
||||
<ion-select-option class="ion-text-wrap" value="sortbycourses">
|
||||
{{'addon.block_timeline.sortbycourses' | translate}}
|
||||
</ion-select-option>
|
||||
</core-combobox>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
|
||||
|
||||
<core-loading [hideUntil]="timeline.loaded" [hidden]="sort != 'sortbydates'">
|
||||
<addon-block-timeline-events [events]="timeline.events" [canLoadMore]="timeline.canLoadMore" (loadMore)="loadMore()"
|
||||
[from]="dataFrom" [to]="dataTo" [overdue]="overdue"></addon-block-timeline-events>
|
||||
</core-loading>
|
||||
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'"
|
||||
[fullscreen]="false" class="safe-area-page margin">
|
||||
<ion-grid class="ion-no-padding">
|
||||
<ion-row class="ion-no-padding">
|
||||
<ion-col *ngFor="let course of timelineCourses.courses" class="ion-no-padding" size="12" size-md="6">
|
||||
<core-courses-course-progress [course]="course">
|
||||
<addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore"
|
||||
(loadMore)="loadMoreCourse(course)" [from]="dataFrom" [to]="dataTo"></addon-block-timeline-events>
|
||||
</core-courses-course-progress>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
|
||||
[message]="'addon.block_timeline.nocoursesinprogress' | translate"></core-empty-box>
|
||||
<core-loading [hideUntil]="timelineCourses.loaded" [hidden]="sort != 'sortbycourses'">
|
||||
<ng-container *ngFor="let course of timelineCourses.courses">
|
||||
<addon-block-timeline-events [events]="course.events" [canLoadMore]="course.canLoadMore" (loadMore)="loadMore(course)"
|
||||
[course]="course" [from]="dataFrom" [to]="dataTo" [overdue]="overdue"></addon-block-timeline-events>
|
||||
</ng-container>
|
||||
<core-empty-box *ngIf="timelineCourses.courses.length == 0" image="assets/img/icons/courses.svg"
|
||||
[message]="'addon.block_timeline.noevents' | translate"></core-empty-box>
|
||||
</core-loading>
|
||||
</core-loading>
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
:host {
|
||||
ion-row.addon-block-timeline-filter {
|
||||
margin: 8px;
|
||||
padding: 0;
|
||||
|
||||
ion-col {
|
||||
padding: 0;
|
||||
margin-right: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
ion-button,
|
||||
core-combobox ::ng-deep ion-button {
|
||||
--border-width: 0;
|
||||
--a11y-min-target-size: 40px;
|
||||
margin: 0;
|
||||
|
||||
.select-icon {
|
||||
display: none;
|
||||
}
|
||||
ion-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
core-combobox ::ng-deep ion-select {
|
||||
margin: 0;
|
||||
--a11y-min-target-size: 40px;
|
||||
}
|
||||
|
||||
core-search-box {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
--height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -13,7 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CoreTimeUtils } from '@services/utils/time';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
||||
import { AddonBlockTimeline } from '../../services/timeline';
|
||||
|
@ -24,6 +23,7 @@ import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/
|
|||
import { CoreSite } from '@classes/site';
|
||||
import { CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
/**
|
||||
* Component to render a timeline block.
|
||||
|
@ -31,12 +31,13 @@ import { CoreCourseOptionsDelegate } from '@features/course/services/course-opti
|
|||
@Component({
|
||||
selector: 'addon-block-timeline',
|
||||
templateUrl: 'addon-block-timeline.html',
|
||||
styleUrls: ['timeline.scss'],
|
||||
})
|
||||
export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implements OnInit {
|
||||
|
||||
sort = 'sortbydates';
|
||||
filter = 'next30days';
|
||||
currentSite?: CoreSite;
|
||||
currentSite!: CoreSite;
|
||||
timeline: {
|
||||
events: AddonCalendarEvent[];
|
||||
loaded: boolean;
|
||||
|
@ -57,24 +58,40 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
|
||||
dataFrom?: number;
|
||||
dataTo?: number;
|
||||
overdue = false;
|
||||
|
||||
protected courseIds: number[] = [];
|
||||
searchEnabled = false;
|
||||
searchText = '';
|
||||
|
||||
protected courseIdsToInvalidate: number[] = [];
|
||||
protected fetchContentDefaultError = 'Error getting timeline data.';
|
||||
protected gradePeriodAfter = 0;
|
||||
protected gradePeriodBefore = 0;
|
||||
|
||||
constructor() {
|
||||
super('AddonBlockTimelineComponent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.currentSite = CoreSites.getCurrentSite();
|
||||
try {
|
||||
this.currentSite = CoreSites.getRequiredCurrentSite();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModal(error);
|
||||
|
||||
this.filter = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||
CoreNavigator.back();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.filter = await this.currentSite.getLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||
this.switchFilter(this.filter);
|
||||
|
||||
this.sort = await this.currentSite!.getLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
||||
this.sort = await this.currentSite.getLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
||||
|
||||
this.searchEnabled = this.currentSite.isVersionGreaterEqualThan('4.0');
|
||||
|
||||
super.ngOnInit();
|
||||
}
|
||||
|
@ -91,8 +108,8 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
promises.push(AddonBlockTimeline.invalidateActionEventsByCourses());
|
||||
promises.push(CoreCourses.invalidateUserCourses());
|
||||
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||
if (this.courseIds.length > 0) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds.join(',')));
|
||||
if (this.courseIdsToInvalidate.length > 0) {
|
||||
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIdsToInvalidate.join(',')));
|
||||
}
|
||||
|
||||
return CoreUtils.allPromises(promises);
|
||||
|
@ -117,28 +134,22 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more events.
|
||||
*/
|
||||
async loadMoreTimeline(): Promise<void> {
|
||||
try {
|
||||
await this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more events.
|
||||
*
|
||||
* @param course Course.
|
||||
* @param course Course. If defined, it will update the course events, timeline otherwise.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadMoreCourse(course: AddonBlockTimelineCourse): Promise<void> {
|
||||
async loadMore(course?: AddonBlockTimelineCourse): Promise<void> {
|
||||
try {
|
||||
const courseEvents = await AddonBlockTimeline.getActionEventsByCourse(course.id, course.canLoadMore);
|
||||
course.events = course.events?.concat(courseEvents.events);
|
||||
course.canLoadMore = courseEvents.canLoadMore;
|
||||
if (course) {
|
||||
const courseEvents =
|
||||
await AddonBlockTimeline.getActionEventsByCourse(course.id, course.canLoadMore, this.searchText);
|
||||
course.events = course.events?.concat(courseEvents.events);
|
||||
course.canLoadMore = courseEvents.canLoadMore;
|
||||
} else {
|
||||
await this.fetchMyOverviewTimeline(this.timeline.canLoadMore);
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError);
|
||||
}
|
||||
|
@ -151,9 +162,9 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchMyOverviewTimeline(afterEventId?: number): Promise<void> {
|
||||
const events = await AddonBlockTimeline.getActionEventsByTimesort(afterEventId);
|
||||
const events = await AddonBlockTimeline.getActionEventsByTimesort(afterEventId, this.searchText);
|
||||
|
||||
this.timeline.events = events.events;
|
||||
this.timeline.events = afterEventId ? this.timeline.events.concat(events.events) : events.events;
|
||||
this.timeline.canLoadMore = events.canLoadMore;
|
||||
}
|
||||
|
||||
|
@ -163,20 +174,36 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchMyOverviewTimelineByCourses(): Promise<void> {
|
||||
const courses = await CoreCoursesHelper.getUserCoursesWithOptions();
|
||||
const today = CoreTimeUtils.timestamp();
|
||||
try {
|
||||
this.gradePeriodAfter = parseInt(await this.currentSite.getConfig('coursegraceperiodafter'), 10);
|
||||
this.gradePeriodBefore = parseInt(await this.currentSite.getConfig('coursegraceperiodbefore'), 10);
|
||||
} catch {
|
||||
this.gradePeriodAfter = 0;
|
||||
this.gradePeriodBefore = 0;
|
||||
}
|
||||
|
||||
this.timelineCourses.courses = courses.filter((course) =>
|
||||
(course.startdate || 0) <= today && (!course.enddate || course.enddate >= today));
|
||||
// Do not filter courses by date because they can contain activities due.
|
||||
this.timelineCourses.courses = await CoreCoursesHelper.getUserCoursesWithOptions();
|
||||
this.courseIdsToInvalidate = this.timelineCourses.courses.map((course) => course.id);
|
||||
|
||||
// Filter only in progress courses.
|
||||
this.timelineCourses.courses = this.timelineCourses.courses.filter((course) =>
|
||||
!course.hidden &&
|
||||
!CoreCoursesHelper.isPastCourse(course, this.gradePeriodAfter) &&
|
||||
!CoreCoursesHelper.isFutureCourse(course, this.gradePeriodAfter, this.gradePeriodBefore));
|
||||
|
||||
if (this.timelineCourses.courses.length > 0) {
|
||||
this.courseIds = this.timelineCourses.courses.map((course) => course.id);
|
||||
const courseEvents = await AddonBlockTimeline.getActionEventsByCourses(this.courseIdsToInvalidate, this.searchText);
|
||||
|
||||
const courseEvents = await AddonBlockTimeline.getActionEventsByCourses(this.courseIds);
|
||||
this.timelineCourses.courses = this.timelineCourses.courses.filter((course) => {
|
||||
if (courseEvents[course.id].events.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.timelineCourses.courses.forEach((course) => {
|
||||
course.events = courseEvents[course.id].events;
|
||||
course.canLoadMore = courseEvents[course.id].canLoadMore;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -188,12 +215,13 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
*/
|
||||
switchFilter(filter: string): void {
|
||||
this.filter = filter;
|
||||
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||
this.currentSite.setLocalSiteConfig('AddonBlockTimelineFilter', this.filter);
|
||||
this.overdue = this.filter === 'overdue';
|
||||
|
||||
switch (this.filter) {
|
||||
case 'overdue':
|
||||
this.dataFrom = -14;
|
||||
this.dataTo = 0;
|
||||
this.dataTo = 1;
|
||||
break;
|
||||
case 'next7days':
|
||||
this.dataFrom = 0;
|
||||
|
@ -226,7 +254,7 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
*/
|
||||
switchSort(sort: string): void {
|
||||
this.sort = sort;
|
||||
this.currentSite?.setLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
||||
this.currentSite.setLocalSiteConfig('AddonBlockTimelineSort', this.sort);
|
||||
|
||||
if (!this.timeline.loaded && this.sort == 'sortbydates') {
|
||||
this.fetchContent();
|
||||
|
@ -235,9 +263,20 @@ export class AddonBlockTimelineComponent extends CoreBlockBaseComponent implemen
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search text changed.
|
||||
*
|
||||
* @param searchValue Search value
|
||||
*/
|
||||
searchTextChanged(searchValue = ''): void {
|
||||
this.searchText = searchValue || '';
|
||||
|
||||
this.fetchContent();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & {
|
||||
export type AddonBlockTimelineCourse = CoreEnrolledCourseDataWithOptions & {
|
||||
events?: AddonCalendarEvent[];
|
||||
canLoadMore?: number;
|
||||
};
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
"next6months": "Next 6 months",
|
||||
"next7days": "Next 7 days",
|
||||
"nocoursesinprogress": "No in-progress courses",
|
||||
"noevents": "No upcoming activities due",
|
||||
"noevents": "No activities require action",
|
||||
"overdue": "Overdue",
|
||||
"pluginname": "Timeline",
|
||||
"searchevents": "Search by activity type or name",
|
||||
"sortbycourses": "Sort by courses",
|
||||
"sortbydates": "Sort by dates"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { CoreCourses } from '@features/courses/services/courses';
|
|||
import { AddonBlockTimelineComponent } from '@addons/block/timeline/components/timeline/timeline';
|
||||
import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonBlockTimeline } from './timeline';
|
||||
import { CoreCoursesDashboard } from '@features/courses/services/dashboard';
|
||||
|
||||
/**
|
||||
* Block handler.
|
||||
|
@ -36,7 +36,7 @@ export class AddonBlockTimelineHandlerService extends CoreBlockBaseHandler {
|
|||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
async isEnabled(): Promise<boolean> {
|
||||
const enabled = await AddonBlockTimeline.isAvailable();
|
||||
const enabled = !CoreCoursesDashboard.isDisabledInSite();
|
||||
const currentSite = CoreSites.getCurrentSite();
|
||||
|
||||
return enabled && ((currentSite && currentSite.isVersionGreaterEqualThan('3.6')) ||
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCoursesDashboard } from '@features/courses/services/dashboard';
|
||||
import {
|
||||
AddonCalendarEvents,
|
||||
AddonCalendarEventsGroupedByCourse,
|
||||
|
@ -26,7 +25,6 @@ import {
|
|||
import moment from 'moment';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreSiteWSPreSets } from '@classes/site';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
|
||||
// Cache key was maintained from block myoverview when blocks were splitted.
|
||||
const ROOT_CACHE_KEY = 'myoverview:';
|
||||
|
@ -45,17 +43,19 @@ export class AddonBlockTimelineProvider {
|
|||
*
|
||||
* @param courseId Only events in this course.
|
||||
* @param afterEventId The last seen event id.
|
||||
* @param searchValue The value a user wishes to search against.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
async getActionEventsByCourse(
|
||||
courseId: number,
|
||||
afterEventId?: number,
|
||||
searchValue = '',
|
||||
siteId?: string,
|
||||
): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const time = moment().subtract(14, 'days').unix(); // Check two weeks ago.
|
||||
const time = this.getDayStart(-14); // Check two weeks ago.
|
||||
|
||||
const data: AddonCalendarGetActionEventsByCourseWSParams = {
|
||||
timesortfrom: time,
|
||||
|
@ -70,17 +70,18 @@ export class AddonBlockTimelineProvider {
|
|||
cacheKey: this.getActionEventsByCourseCacheKey(courseId),
|
||||
};
|
||||
|
||||
if (searchValue != '') {
|
||||
data.searchvalue = searchValue;
|
||||
preSets.getFromCache = false;
|
||||
}
|
||||
|
||||
const courseEvents = await site.read<AddonCalendarEvents>(
|
||||
'core_calendar_get_action_events_by_course',
|
||||
data,
|
||||
preSets,
|
||||
);
|
||||
|
||||
if (courseEvents && courseEvents.events) {
|
||||
return this.treatCourseEvents(courseEvents, time);
|
||||
}
|
||||
|
||||
throw new CoreError('No events returned on core_calendar_get_action_events_by_course.');
|
||||
return this.treatCourseEvents(courseEvents, time);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,15 +99,17 @@ export class AddonBlockTimelineProvider {
|
|||
*
|
||||
* @param courseIds Course IDs.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @param searchValue The value a user wishes to search against.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
async getActionEventsByCourses(
|
||||
courseIds: number[],
|
||||
searchValue = '',
|
||||
siteId?: string,
|
||||
): Promise<{[courseId: string]: { events: AddonCalendarEvent[]; canLoadMore?: number } }> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const time = moment().subtract(14, 'days').unix(); // Check two weeks ago.
|
||||
const time = this.getDayStart(-14); // Check two weeks ago.
|
||||
|
||||
const data: AddonCalendarGetActionEventsByCoursesWSParams = {
|
||||
timesortfrom: time,
|
||||
|
@ -117,6 +120,11 @@ export class AddonBlockTimelineProvider {
|
|||
cacheKey: this.getActionEventsByCoursesCacheKey(),
|
||||
};
|
||||
|
||||
if (searchValue != '') {
|
||||
data.searchvalue = searchValue;
|
||||
preSets.getFromCache = false;
|
||||
}
|
||||
|
||||
const events = await site.read<AddonCalendarEventsGroupedByCourse>(
|
||||
'core_calendar_get_action_events_by_courses',
|
||||
data,
|
||||
|
@ -145,16 +153,18 @@ export class AddonBlockTimelineProvider {
|
|||
* Get calendar action events based on the timesort value.
|
||||
*
|
||||
* @param afterEventId The last seen event id.
|
||||
* @param searchValue The value a user wishes to search against.
|
||||
* @param siteId Site ID. If not defined, use current site.
|
||||
* @return Promise resolved when the info is retrieved.
|
||||
*/
|
||||
async getActionEventsByTimesort(
|
||||
afterEventId?: number,
|
||||
searchValue = '',
|
||||
siteId?: string,
|
||||
): Promise<{ events: AddonCalendarEvent[]; canLoadMore?: number }> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
const timesortfrom = moment().subtract(14, 'days').unix(); // Check two weeks ago.
|
||||
const timesortfrom = this.getDayStart(-14); // Check two weeks ago.
|
||||
const limitnum = AddonBlockTimelineProvider.EVENTS_LIMIT;
|
||||
|
||||
const data: AddonCalendarGetActionEventsByTimesortWSParams = {
|
||||
|
@ -171,25 +181,27 @@ export class AddonBlockTimelineProvider {
|
|||
uniqueCacheKey: true,
|
||||
};
|
||||
|
||||
if (searchValue != '') {
|
||||
data.searchvalue = searchValue;
|
||||
preSets.getFromCache = false;
|
||||
preSets.cacheKey += ':' + searchValue;
|
||||
}
|
||||
|
||||
const result = await site.read<AddonCalendarEvents>(
|
||||
'core_calendar_get_action_events_by_timesort',
|
||||
data,
|
||||
preSets,
|
||||
);
|
||||
|
||||
if (result && result.events) {
|
||||
const canLoadMore = result.events.length >= limitnum ? result.lastid : undefined;
|
||||
const canLoadMore = result.events.length >= limitnum ? result.lastid : undefined;
|
||||
|
||||
// Filter events by time in case it uses cache.
|
||||
const events = result.events.filter((element) => element.timesort >= timesortfrom);
|
||||
// Filter events by time in case it uses cache.
|
||||
const events = result.events.filter((element) => element.timesort >= timesortfrom);
|
||||
|
||||
return {
|
||||
events,
|
||||
canLoadMore,
|
||||
};
|
||||
}
|
||||
|
||||
throw new CoreError('No events returned on core_calendar_get_action_events_by_timesort.');
|
||||
return {
|
||||
events,
|
||||
canLoadMore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -239,24 +251,6 @@ export class AddonBlockTimelineProvider {
|
|||
await site.invalidateWsCacheForKeyStartingWith(this.getActionEventsByTimesortPrefixCacheKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not My Overview is available for a certain site.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with true if available, resolved with false or rejected otherwise.
|
||||
*/
|
||||
async isAvailable(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
// First check if dashboard is disabled.
|
||||
if (CoreCoursesDashboard.isDisabledInSite(site)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return site.wsAvailable('core_calendar_get_action_events_by_courses') &&
|
||||
site.wsAvailable('core_calendar_get_action_events_by_timesort');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles course events, filtering and treating if more can be loaded.
|
||||
*
|
||||
|
@ -281,6 +275,16 @@ export class AddonBlockTimelineProvider {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp at the start of the day with an optional offset.
|
||||
*
|
||||
* @param daysOffset Offset days to add or substract.
|
||||
* @return timestamp.
|
||||
*/
|
||||
getDayStart(daysOffset = 0): number {
|
||||
return moment().startOf('day').add(daysOffset, 'days').unix();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const AddonBlockTimeline = makeSingleton(AddonBlockTimelineProvider);
|
||||
|
|
|
@ -22,6 +22,7 @@ import { CoreCommentsComponentsModule } from '@features/comments/components/comp
|
|||
import { CoreTagComponentsModule } from '@features/tag/components/components.module';
|
||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { AddonBlogMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||
import { CoreMainMenuComponentsModule } from '@features/mainmenu/components/components.module';
|
||||
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
return [
|
||||
|
@ -39,6 +40,7 @@ function buildRoutes(injector: Injector): Routes {
|
|||
CoreSharedModule,
|
||||
CoreCommentsComponentsModule,
|
||||
CoreTagComponentsModule,
|
||||
CoreMainMenuComponentsModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
providers: [
|
||||
|
|
|
@ -51,8 +51,7 @@ const routes: Routes = [
|
|||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => async () => {
|
||||
useValue: () => {
|
||||
CoreContentLinksDelegate.registerHandler(AddonBlogIndexLinkHandler.instance);
|
||||
CoreMainMenuDelegate.registerHandler(AddonBlogMainMenuHandler.instance);
|
||||
CoreUserDelegate.registerHandler(AddonBlogUserHandler.instance);
|
||||
|
|
|
@ -3,21 +3,24 @@
|
|||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<h1>{{ title | translate }}</h1>
|
||||
<ion-buttons slot="end"></ion-buttons>
|
||||
<ion-title>
|
||||
<h1>{{ title | translate }}</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<core-user-menu-button></core-user-menu-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-content class="limited-width">
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<ion-item *ngIf="showMyEntriesToggle">
|
||||
<ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label>
|
||||
<ion-label>{{ 'addon.blog.showonlyyourentries' | translate }}</ion-label>
|
||||
<ion-toggle [(ngModel)]="onlyMyEntries" (ionChange)="onlyMyEntriesToggleChanged(onlyMyEntries)"></ion-toggle>
|
||||
</ion-item>
|
||||
<core-empty-box *ngIf="entries && entries.length == 0" icon="far-newspaper"
|
||||
[message]="'addon.blog.noentriesyet' | translate">
|
||||
<core-empty-box *ngIf="entries && entries.length == 0" icon="far-newspaper" [message]="'addon.blog.noentriesyet' | translate">
|
||||
</core-empty-box>
|
||||
<ng-container *ngFor="let entry of entries">
|
||||
<ion-card *ngIf="!onlyMyEntries || entry.userid == currentUserId">
|
||||
|
@ -25,8 +28,7 @@
|
|||
<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 [text]="entry.subject" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId">
|
||||
</core-format-text>
|
||||
<ion-note class="ion-float-end ion-padding-start ion-text-end">
|
||||
{{ 'addon.blog.' + entry.publishTranslated! | translate}}
|
||||
|
@ -66,8 +68,9 @@
|
|||
</ion-card-content>
|
||||
<div class="ion-text-center ion-margin-bottom" *ngIf="entry.lastmodified > entry.created">
|
||||
<ion-note>
|
||||
<ion-icon name="fas-clock"
|
||||
[attr.aria-label]="'core.lastmodified' | translate"></ion-icon> {{entry.lastmodified | coreTimeAgo}}
|
||||
<ion-icon name="fas-clock" [attr.aria-label]="'core.lastmodified' | translate"></ion-icon> {{entry.lastmodified
|
||||
|
|
||||
coreTimeAgo}}
|
||||
</ion-note>
|
||||
</div>
|
||||
</ion-card>
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ContextLevel } from '@/core/constants';
|
|||
import { AddonBlog, AddonBlogFilter, AddonBlogPost, AddonBlogProvider } from '@addons/blog/services/blog';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CoreComments } from '@features/comments/services/comments';
|
||||
import { CoreMainMenuDeepLinkManager } from '@features/mainmenu/classes/deep-link-manager';
|
||||
import { CoreTag } from '@features/tag/services/tag';
|
||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
|
@ -42,6 +43,7 @@ export class AddonBlogEntriesPage implements OnInit {
|
|||
protected canLoadMoreEntries = false;
|
||||
protected canLoadMoreUserEntries = true;
|
||||
protected siteHomeId: number;
|
||||
protected fetchSuccess = false;
|
||||
|
||||
loaded = false;
|
||||
canLoadMore = false;
|
||||
|
@ -118,9 +120,10 @@ export class AddonBlogEntriesPage implements OnInit {
|
|||
this.commentsEnabled = !CoreComments.areCommentsDisabledInSite();
|
||||
this.tagsEnabled = CoreTag.areTagsAvailableInSite();
|
||||
|
||||
await this.fetchEntries();
|
||||
const deepLinkManager = new CoreMainMenuDeepLinkManager();
|
||||
deepLinkManager.treatLink();
|
||||
|
||||
CoreUtils.ignoreErrors(AddonBlog.logView(this.filter));
|
||||
await this.fetchEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -170,15 +173,9 @@ export class AddonBlogEntriesPage implements OnInit {
|
|||
entry.contextInstanceId = entry.userid;
|
||||
}
|
||||
|
||||
entry.summary = CoreTextUtils.instance.replacePluginfileUrls(entry.summary, entry.summaryfiles || []);
|
||||
entry.summary = CoreTextUtils.replacePluginfileUrls(entry.summary, entry.summaryfiles || []);
|
||||
|
||||
return CoreUser.getProfile(entry.userid, entry.courseid, true).then((user) => {
|
||||
entry.user = user;
|
||||
|
||||
return;
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
entry.user = await CoreUtils.ignoreErrors(CoreUser.getProfile(entry.userid, entry.courseid, true));
|
||||
});
|
||||
|
||||
if (refresh) {
|
||||
|
@ -201,6 +198,11 @@ export class AddonBlogEntriesPage implements OnInit {
|
|||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
if (!this.fetchSuccess) {
|
||||
this.fetchSuccess = true;
|
||||
CoreUtils.ignoreErrors(AddonBlog.logView(this.filter));
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.blog.errorloadentries', true);
|
||||
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||
|
|
|
@ -40,11 +40,12 @@ export class AddonBlogProvider {
|
|||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with true if enabled, resolved with false or rejected otherwise.
|
||||
* @since 3.6
|
||||
*/
|
||||
async isPluginEnabled(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.getSite(siteId);
|
||||
|
||||
return site.wsAvailable('core_blog_get_entries') &&site.canUseAdvancedFeature('enableblogs');
|
||||
return site.wsAvailable('core_blog_get_entries') && site.canUseAdvancedFeature('enableblogs');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -62,7 +62,7 @@ export class AddonBlogCourseOptionHandlerService implements CoreCourseOptionsHan
|
|||
): Promise<boolean> {
|
||||
const enabled = await CoreCourseHelper.hasABlockNamed(courseId, 'blog_menu');
|
||||
|
||||
if (enabled && navOptions && typeof navOptions.blogs != 'undefined') {
|
||||
if (enabled && navOptions && navOptions.blogs !== undefined) {
|
||||
return navOptions.blogs;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ export class AddonBlogMainMenuHandlerService implements CoreMainMenuHandler {
|
|||
static readonly PAGE_NAME = 'blog';
|
||||
|
||||
name = 'AddonBlog';
|
||||
priority = 450;
|
||||
priority = 500;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
|
|
|
@ -13,8 +13,14 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreUserProfileHandler, CoreUserProfileHandlerData, CoreUserDelegateService } from '@features/user/services/user-delegate';
|
||||
import {
|
||||
CoreUserProfileHandler,
|
||||
CoreUserProfileHandlerData,
|
||||
CoreUserDelegateService,
|
||||
CoreUserDelegateContext,
|
||||
} from '@features/user/services/user-delegate';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { AddonBlog } from '../blog';
|
||||
|
||||
|
@ -24,8 +30,8 @@ import { AddonBlog } from '../blog';
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class AddonBlogUserHandlerService implements CoreUserProfileHandler {
|
||||
|
||||
name = 'AddonBlog:blogs';
|
||||
priority = 300;
|
||||
name = 'AddonBlog'; // This name doesn't match any disabled feature, they'll be checked in isEnabledForContext.
|
||||
priority = 200;
|
||||
type = CoreUserDelegateService.TYPE_NEW_PAGE;
|
||||
|
||||
/**
|
||||
|
@ -35,6 +41,27 @@ export class AddonBlogUserHandlerService implements CoreUserProfileHandler {
|
|||
return AddonBlog.isPluginEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async isEnabledForContext(context: CoreUserDelegateContext): Promise<boolean> {
|
||||
// Check if feature is disabled.
|
||||
const currentSite = CoreSites.getCurrentSite();
|
||||
if (!currentSite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context === CoreUserDelegateContext.USER_MENU) {
|
||||
if (currentSite.isFeatureDisabled('CoreUserDelegate_AddonBlog:account')) {
|
||||
return false;
|
||||
}
|
||||
} else if (currentSite.isFeatureDisabled('CoreUserDelegate_AddonBlog:blogs')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -43,11 +70,11 @@ export class AddonBlogUserHandlerService implements CoreUserProfileHandler {
|
|||
icon: 'far-newspaper',
|
||||
title: 'addon.blog.blogentries',
|
||||
class: 'addon-blog-handler',
|
||||
action: (event, user, courseId): void => {
|
||||
action: (event, user, context, contextId): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
CoreNavigator.navigateToSitePath('/blog', {
|
||||
params: { courseId, userId: user.id },
|
||||
params: { courseId: contextId, userId: user.id },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
|
||||
--addon-calendar-blank-day-background-color: var(--gray-lighter);
|
||||
--addon-calendar-blank-day-background-color: var(--light);
|
||||
|
||||
.item.addon-calendar-event {
|
||||
> ion-icon {
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 6px;
|
||||
padding: 0.7rem;
|
||||
--margin-vertical: 12px;
|
||||
--margin-end: 12px;
|
||||
margin-top: var(--margin-vertical);
|
||||
margin-bottom: var(--margin-vertical);
|
||||
@include margin-horizontal(null, var(--margin-end));
|
||||
|
||||
}
|
||||
|
||||
&.addon-calendar-eventtype-category > ion-icon {
|
||||
background-color: var(--addon-calendar-event-category-color);
|
||||
}
|
||||
&.addon-calendar-eventtype-course > ion-icon {
|
||||
background-color: var(--addon-calendar-event-course-color);
|
||||
}
|
||||
&.addon-calendar-eventtype-group > ion-icon {
|
||||
background-color: var(--addon-calendar-event-group-color);
|
||||
}
|
||||
&.addon-calendar-eventtype-user > ion-icon {
|
||||
background-color: var(--addon-calendar-event-user-color);
|
||||
}
|
||||
&.addon-calendar-eventtype-site > ion-icon {
|
||||
background-color: var(--addon-calendar-event-site-color);
|
||||
@each $category, $value in $calendar-event-category-colors {
|
||||
&.addon-calendar-eventtype-#{$category} > ion-icon {
|
||||
background-color: $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,22 +13,11 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injector, NgModule } from '@angular/core';
|
||||
import { Route, RouterModule, ROUTES, Routes } from '@angular/router';
|
||||
import { RouterModule, ROUTES, Routes } from '@angular/router';
|
||||
|
||||
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||
import { AddonCalendarMainMenuHandlerService } from './services/handlers/mainmenu';
|
||||
|
||||
export const AddonCalendarEditRoute: Route = {
|
||||
path: 'edit/:eventId',
|
||||
loadChildren: () =>
|
||||
import('@/addons/calendar/pages/edit-event/edit-event.module').then(m => m.AddonCalendarEditEventPageModule),
|
||||
};
|
||||
|
||||
export const AddonCalendarEventRoute: Route ={
|
||||
path: 'event/:id',
|
||||
loadChildren: () => import('@/addons/calendar/pages/event/event.module').then(m => m.AddonCalendarEventPageModule),
|
||||
};
|
||||
|
||||
function buildRoutes(injector: Injector): Routes {
|
||||
return [
|
||||
{
|
||||
|
@ -36,27 +25,27 @@ function buildRoutes(injector: Injector): Routes {
|
|||
data: {
|
||||
mainMenuTabRoot: AddonCalendarMainMenuHandlerService.PAGE_NAME,
|
||||
},
|
||||
loadChildren: () => import('@/addons/calendar/pages/index/index.module').then(m => m.AddonCalendarIndexPageModule),
|
||||
loadChildren: () => import('@addons/calendar/pages/index/index.module').then(m => m.AddonCalendarIndexPageModule),
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
data: {
|
||||
mainMenuTabRoot: AddonCalendarMainMenuHandlerService.PAGE_NAME,
|
||||
},
|
||||
loadChildren: () => import('@/addons/calendar/pages/list/list.module').then(m => m.AddonCalendarListPageModule),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
path: 'calendar-settings',
|
||||
loadChildren: () =>
|
||||
import('@/addons/calendar/pages/settings/settings.module').then(m => m.AddonCalendarSettingsPageModule),
|
||||
import('@addons/calendar/pages/settings/settings.module').then(m => m.AddonCalendarSettingsPageModule),
|
||||
},
|
||||
{
|
||||
path: 'day',
|
||||
loadChildren: () =>
|
||||
import('@/addons/calendar/pages/day/day.module').then(m => m.AddonCalendarDayPageModule),
|
||||
import('@addons/calendar/pages/day/day.module').then(m => m.AddonCalendarDayPageModule),
|
||||
},
|
||||
{
|
||||
path: 'event/:id',
|
||||
loadChildren: () => import('@addons/calendar/pages/event/event.module').then(m => m.AddonCalendarEventPageModule),
|
||||
},
|
||||
{
|
||||
path: 'edit/:eventId',
|
||||
loadChildren: () =>
|
||||
import('@addons/calendar/pages/edit-event/edit-event.module').then(m => m.AddonCalendarEditEventPageModule),
|
||||
},
|
||||
AddonCalendarEventRoute,
|
||||
AddonCalendarEditRoute,
|
||||
...buildTabMainRoutes(injector, {
|
||||
redirectTo: 'index',
|
||||
pathMatch: 'full',
|
||||
|
|
|
@ -63,8 +63,7 @@ const mainMenuChildrenRoutes: Routes = [
|
|||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [],
|
||||
useFactory: () => async () => {
|
||||
useValue: async () => {
|
||||
CoreContentLinksDelegate.registerHandler(AddonCalendarViewLinkHandler.instance);
|
||||
CoreMainMenuDelegate.registerHandler(AddonCalendarMainMenuHandler.instance);
|
||||
CoreCronDelegate.register(AddonCalendarSyncCronHandler.instance);
|
||||
|
|
|
@ -1,112 +1,110 @@
|
|||
|
||||
<!-- Add buttons to the nav bar. -->
|
||||
<core-navbar-buttons slot="end" prepend>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="canNavigate && !isCurrentMonth && displayNavButtons" [priority]="900"
|
||||
[content]="'addon.calendar.currentmonth' | translate" iconAction="fas-calendar-day"
|
||||
(action)="goToCurrentMonth()"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="canNavigate && !selectedMonthIsCurrent() && displayNavButtons" [priority]="900"
|
||||
[content]="'addon.calendar.currentmonth' | translate" iconAction="fas-calendar-day" (action)="goToCurrentMonth()">
|
||||
</core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
|
||||
<core-loading [hideUntil]="loaded" class="safe-area-page">
|
||||
<!-- Period name and arrows to navigate. -->
|
||||
<ion-grid class="ion-no-padding addon-calendar-navigation">
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col class="ion-text-start" *ngIf="canNavigate">
|
||||
<ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'core.previous' | translate">
|
||||
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-center addon-calendar-period">
|
||||
<h2 id="addon-calendar-monthname">{{ periodName }}</h2>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-end" *ngIf="canNavigate">
|
||||
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate">
|
||||
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
|
||||
<!-- Calendar view. -->
|
||||
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
|
||||
<div role="rowgroup">
|
||||
<!-- List of days. -->
|
||||
<ion-row role="row">
|
||||
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of weekDays" role="columnheader">
|
||||
<span class="sr-only">{{ day.fullname | translate }}</span>
|
||||
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
|
||||
<span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
|
||||
<core-loading [hideUntil]="loaded">
|
||||
<div class="core-swipe-slides-container">
|
||||
<!-- Period name and arrows to navigate. -->
|
||||
<ion-grid class="ion-no-padding addon-calendar-navigation">
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col class="ion-text-start" *ngIf="canNavigate">
|
||||
<ion-button fill="clear" (click)="loadPrevious()" [attr.aria-label]="'core.previous' | translate">
|
||||
<ion-icon name="fas-chevron-left" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-center addon-calendar-period">
|
||||
<h2 id="addon-calendar-monthname">
|
||||
{{ periodName }}
|
||||
<ion-spinner *ngIf="!selectedMonthLoaded()" class="addon-calendar-loading-month">
|
||||
</ion-spinner>
|
||||
</h2>
|
||||
</ion-col>
|
||||
<ion-col class="ion-text-end" *ngIf="canNavigate">
|
||||
<ion-button fill="clear" (click)="loadNext()" [attr.aria-label]="'core.next' | translate">
|
||||
<ion-icon name="fas-chevron-right" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
<div role="rowgroup">
|
||||
</ion-grid>
|
||||
|
||||
<!-- Weeks. -->
|
||||
<ion-row *ngFor="let week of weeks" class="addon-calendar-week" role="row">
|
||||
<!-- Empty slots (first week). -->
|
||||
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell"></ion-col>
|
||||
<ion-col
|
||||
*ngFor="let day of week.days"
|
||||
class="addon-calendar-day ion-text-center"
|
||||
[ngClass]='{
|
||||
"hasevents": day.hasevents,
|
||||
"today": isCurrentMonth && day.istoday,
|
||||
"weekend": day.isweekend,
|
||||
"duration_finish": day.haslastdayofevent
|
||||
}'
|
||||
[class.addon-calendar-event-past-day]="isPastMonth || day.ispast"
|
||||
role="cell"
|
||||
tabindex="0"
|
||||
(ariaButtonClick)="dayClicked(day.mday)"
|
||||
>
|
||||
<p class="addon-calendar-day-number" role="button">
|
||||
<span aria-hidden="true">{{ day.mday }}</span>
|
||||
<span class="sr-only">{{ day.periodName | translate }}</span>
|
||||
</p>
|
||||
|
||||
<!-- In phone, display some dots to indicate the type of events. -->
|
||||
<p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
|
||||
class="calendar_event_type calendar_event_{{type}}"></span></p>
|
||||
|
||||
<!-- In tablet, display list of events. -->
|
||||
<div class="ion-hide-md-down addon-calendar-day-events">
|
||||
<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)"
|
||||
>
|
||||
<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>
|
||||
<ion-icon *ngIf="event.deleted" name="fas-trash"
|
||||
[attr.aria-label]="'core.deletedoffline' | translate"></ion-icon>
|
||||
<span class="addon-calendar-event-time">
|
||||
{{ event.timestart * 1000 | coreFormatDate: timeFormat }}
|
||||
</span>
|
||||
<img *ngIf="event.moduleIcon" src="{{event.moduleIcon}}" alt="" role="presentation"
|
||||
class="core-module-icon">
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
<span class="sr-only">
|
||||
{{ 'addon.calendar.type' + event.formattedType | translate }}
|
||||
<span class="sr-only" *ngIf="event.moduleIcon && event.iconTitle">{{ event.iconTitle }}</span>
|
||||
</span>
|
||||
<span class="addon-calendar-event-name">{{event.name}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more">
|
||||
<b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b>
|
||||
</p>
|
||||
<core-swipe-slides [manager]="manager">
|
||||
<ng-template let-month="item">
|
||||
<!-- Calendar view. -->
|
||||
<ion-grid class="addon-calendar-months" role="table" aria-describedby="addon-calendar-monthname">
|
||||
<div role="rowgroup">
|
||||
<!-- List of days. -->
|
||||
<ion-row role="row">
|
||||
<ion-col class="ion-text-center addon-calendar-weekday" *ngFor="let day of month.weekDays" role="columnheader">
|
||||
<span class="sr-only">{{ day.fullname | translate }}</span>
|
||||
<span class="ion-hide-md-up" aria-hidden="true">{{ day.shortname | translate }}</span>
|
||||
<span class="ion-hide-md-down" aria-hidden="true">{{ day.fullname | translate }}</span>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
</ion-col>
|
||||
<!-- Empty slots (last week). -->
|
||||
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell"></ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
</ion-grid>
|
||||
<div role="rowgroup">
|
||||
<!-- Weeks. -->
|
||||
<ion-row *ngFor="let week of month.weeks" class="addon-calendar-week" role="row">
|
||||
<!-- Empty slots (first week). -->
|
||||
<ion-col *ngFor="let value of week.prepadding" class="dayblank addon-calendar-day" role="cell">
|
||||
</ion-col>
|
||||
<ion-col *ngFor="let day of week.days" class="addon-calendar-day ion-text-center" [ngClass]='{
|
||||
"hasevents": day.hasevents,
|
||||
"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">
|
||||
<span aria-hidden="true">{{ day.mday }}</span>
|
||||
<span class="sr-only">{{ day.periodName | translate }}</span>
|
||||
</p>
|
||||
|
||||
<!-- In phone, display some dots to indicate the type of events. -->
|
||||
<p class="ion-hide-md-up addon-calendar-dot-types"><span *ngFor="let type of day.calendareventtypes"
|
||||
class="calendar_event_type calendar_event_{{type}}"></span></p>
|
||||
|
||||
<!-- In tablet, display list of events. -->
|
||||
<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)">
|
||||
<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>
|
||||
<ion-icon *ngIf="event.deleted" name="fas-trash"
|
||||
[attr.aria-label]="'core.deletedoffline' | translate"></ion-icon>
|
||||
<span class="addon-calendar-event-time">
|
||||
{{ event.timestart * 1000 | coreFormatDate: timeFormat }}
|
||||
</span>
|
||||
<!-- Add the icon title so accessibility tools read it. -->
|
||||
<span class="sr-only">
|
||||
{{ 'addon.calendar.type' + event.formattedType | translate }}
|
||||
<span class="sr-only" *ngIf="event.iconTitle">
|
||||
{{ event.iconTitle }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="addon-calendar-event-name">{{event.name}}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<p *ngIf="day.filteredEvents.length > 4" class="addon-calendar-day-more">
|
||||
<b>{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}</b>
|
||||
</p>
|
||||
</div>
|
||||
</ion-col>
|
||||
<!-- Empty slots (last week). -->
|
||||
<ion-col *ngFor="let value of week.postpadding" class="dayblank addon-calendar-day" role="cell">
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</div>
|
||||
</ion-grid>
|
||||
</ng-template>
|
||||
</core-swipe-slides>
|
||||
</div>
|
||||
|
||||
</core-loading>
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
--addon-calendar-blank-day-background-color: var(--gray-lighter);
|
||||
--addon-calendar-blank-day-background-color: var(--light);
|
||||
|
||||
.core-swipe-slides-container ion-grid {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.addon-calendar-navigation {
|
||||
padding-top: 5px;
|
||||
|
@ -10,22 +17,22 @@
|
|||
.addon-calendar-months {
|
||||
background-color: var(--contrast-background);
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
font-size: var(--text-size);
|
||||
}
|
||||
|
||||
.addon-calendar-day {
|
||||
border-bottom: 1px solid var(--addon-calendar-border-color);
|
||||
border-right: 1px solid var(--addon-calendar-border-color);
|
||||
@include border-end(1px, solid var(--addon-calendar-border-color));
|
||||
overflow: hidden;
|
||||
min-height: 60px;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
padding-left: 10px;
|
||||
@include padding-horizontal(10px, null);
|
||||
}
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
padding-left: 8px;
|
||||
@include border-end(0);
|
||||
@include padding-horizontal(8px, null);
|
||||
}
|
||||
|
||||
&.addon-calendar-event-past-day > .addon-calendar-dot-types,
|
||||
|
@ -48,7 +55,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@include media-breakpoint-up(md) {
|
||||
.addon-calendar-day-number {
|
||||
text-align: start;
|
||||
}
|
||||
|
@ -56,7 +63,7 @@
|
|||
|
||||
&.today .addon-calendar-day-number span {
|
||||
border: 2px solid var(--addon-calendar-today-border-color);
|
||||
line-height: 20px;;
|
||||
line-height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
&.dayblank {
|
||||
|
@ -82,9 +89,7 @@
|
|||
}
|
||||
|
||||
.addon-calendar-day-more {
|
||||
margin-top: 0.6em;
|
||||
margin-bottom: 0.6em;
|
||||
margin-right: 4px;
|
||||
@include margin(0.6em, null, 0.6em, 4px);
|
||||
}
|
||||
|
||||
.addon-calendar-dot-types {
|
||||
|
@ -98,6 +103,10 @@
|
|||
margin-top: 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.addon-calendar-loading-month {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-calendar-weekday {
|
||||
|
@ -106,10 +115,10 @@
|
|||
}
|
||||
|
||||
.addon-calendar-day-events {
|
||||
text-align: left;
|
||||
text-align: start;
|
||||
|
||||
ion-icon {
|
||||
margin-right: 2px;
|
||||
@include margin-horizontal(null, 2px);
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
@ -127,66 +136,22 @@
|
|||
margin-right: 1px;
|
||||
margin-left: 1px;
|
||||
|
||||
&.calendar_event_category {
|
||||
background-color: var(--addon-calendar-event-category-color);
|
||||
}
|
||||
&.calendar_event_course {
|
||||
background-color: var(--addon-calendar-event-course-color);
|
||||
}
|
||||
&.calendar_event_group {
|
||||
background-color: var(--addon-calendar-event-group-color);
|
||||
}
|
||||
&.calendar_event_user {
|
||||
background-color: var(--addon-calendar-event-user-color);
|
||||
}
|
||||
&.calendar_event_site {
|
||||
background-color: var(--addon-calendar-event-site-color);
|
||||
@each $category, $value in $calendar-event-category-colors {
|
||||
&.calendar_event_#{$category} {
|
||||
background-color: $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.core-module-icon {
|
||||
margin-right: 1px;
|
||||
margin-left: 1px;
|
||||
--size: 16px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.core-module-icon[slot="start"] {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context([dir=rtl]) {
|
||||
.addon-calendar-day-events {
|
||||
text-align: right;
|
||||
|
||||
ion-icon {
|
||||
margin-right: unset;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.addon-calendar-day {
|
||||
border-left: 1px solid var(--addon-calendar-border-color);
|
||||
border-right: unset;
|
||||
|
||||
&:first-child {
|
||||
padding-right: 10px;
|
||||
padding-left: unset;
|
||||
}
|
||||
&:last-child {
|
||||
border-left: 0;
|
||||
border-right: unset;
|
||||
padding-right: 8px;
|
||||
padding-left: unset;
|
||||
}
|
||||
.addon-calendar-day-more {
|
||||
margin-left: 4px;
|
||||
margin-right: unset;
|
||||
}
|
||||
ion-slide {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(body.dark) {
|
||||
--addon-calendar-blank-day-background-color: var(--black);
|
||||
--addon-calendar-blank-day-background-color: var(--gray-900);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import {
|
|||
EventEmitter,
|
||||
KeyValueDiffers,
|
||||
KeyValueDiffer,
|
||||
ViewChild,
|
||||
HostBinding,
|
||||
} from '@angular/core';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
|
@ -40,7 +42,13 @@ import { AddonCalendarFilter, AddonCalendarHelper } from '../../services/calenda
|
|||
import { AddonCalendarOffline } from '../../services/calendar-offline';
|
||||
import { CoreCategoryData, CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||
import { CoreSwipeSlidesComponent } from '@components/swipe-slides/swipe-slides';
|
||||
import {
|
||||
CoreSwipeSlidesDynamicItem,
|
||||
CoreSwipeSlidesDynamicItemsManagerSource,
|
||||
} from '@classes/items-management/swipe-slides-dynamic-items-manager-source';
|
||||
import { CoreSwipeSlidesDynamicItemsManager } from '@classes/items-management/swipe-slides-dynamic-items-manager';
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* Component that displays a calendar.
|
||||
|
@ -52,54 +60,31 @@ import { CoreLocalNotifications } from '@services/local-notifications';
|
|||
})
|
||||
export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestroy {
|
||||
|
||||
@ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent<PreloadedMonth>;
|
||||
|
||||
@Input() initialYear?: number; // Initial year to load.
|
||||
@Input() initialMonth?: number; // Initial month to load.
|
||||
@Input() filter?: AddonCalendarFilter; // Filter to apply.
|
||||
@Input() hidden?: boolean; // Whether the component is hidden.
|
||||
@Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true.
|
||||
@Input() displayNavButtons?: string | boolean; // Whether to display nav buttons created by this component. Defaults to true.
|
||||
@Output() onEventClicked = new EventEmitter<number>();
|
||||
@Output() onDayClicked = new EventEmitter<{day: number; month: number; year: number}>();
|
||||
|
||||
periodName?: string;
|
||||
weekDays: AddonCalendarWeekDaysTranslationKeys[] = [];
|
||||
weeks: AddonCalendarWeek[] = [];
|
||||
manager?: CoreSwipeSlidesDynamicItemsManager<PreloadedMonth, AddonCalendarMonthSlidesItemsManagerSource>;
|
||||
loaded = false;
|
||||
timeFormat?: string;
|
||||
isCurrentMonth = false;
|
||||
isPastMonth = false;
|
||||
|
||||
protected year?: number;
|
||||
protected month?: number;
|
||||
protected categoriesRetrieved = false;
|
||||
protected categories: { [id: number]: CoreCategoryData } = {};
|
||||
protected currentSiteId: string;
|
||||
protected offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } =
|
||||
{}; // Offline events classified in month & day.
|
||||
|
||||
protected offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
|
||||
protected deletedEvents: number[] = []; // Events deleted in offline.
|
||||
protected currentTime?: number;
|
||||
protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the data input.
|
||||
// Observers.
|
||||
protected hiddenDiffer?: boolean; // To detect changes in the hidden input.
|
||||
protected filterDiffer: KeyValueDiffer<unknown, unknown>; // To detect changes in the filters input.
|
||||
// Observers and listeners.
|
||||
protected undeleteEventObserver: CoreEventObserver;
|
||||
protected obsDefaultTimeChange?: CoreEventObserver;
|
||||
protected managerUnsubscribe?: () => void;
|
||||
|
||||
constructor(
|
||||
differs: KeyValueDiffers,
|
||||
) {
|
||||
constructor(differs: KeyValueDiffers) {
|
||||
this.currentSiteId = CoreSites.getCurrentSiteId();
|
||||
|
||||
if (CoreLocalNotifications.isAvailable()) {
|
||||
// Re-schedule events if default time changes.
|
||||
this.obsDefaultTimeChange = CoreEvents.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
AddonCalendar.scheduleEventsNotifications(day.eventsFormated!);
|
||||
});
|
||||
});
|
||||
}, this.currentSiteId);
|
||||
}
|
||||
|
||||
// Listen for events "undeleted" (offline).
|
||||
this.undeleteEventObserver = CoreEvents.on(
|
||||
AddonCalendarProvider.UNDELETED_EVENT_EVENT,
|
||||
|
@ -112,27 +97,40 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
this.undeleteEvent(data.eventId);
|
||||
|
||||
// Remove it from the list of deleted events if it's there.
|
||||
const index = this.deletedEvents.indexOf(data.eventId);
|
||||
const index = this.manager?.getSource().deletedEvents.indexOf(data.eventId) ?? -1;
|
||||
if (index != -1) {
|
||||
this.deletedEvents.splice(index, 1);
|
||||
this.manager?.getSource().deletedEvents.splice(index, 1);
|
||||
}
|
||||
},
|
||||
this.currentSiteId,
|
||||
);
|
||||
|
||||
this.differ = differs.find([]).create();
|
||||
this.hiddenDiffer = this.hidden;
|
||||
this.filterDiffer = differs.find(this.filter ?? {}).create();
|
||||
}
|
||||
|
||||
@HostBinding('attr.hidden') get hiddenAttribute(): string | null {
|
||||
return this.hidden ? 'hidden' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const now = new Date();
|
||||
this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate);
|
||||
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
|
||||
CoreUtils.isTrueOrOne(this.displayNavButtons);
|
||||
|
||||
this.year = this.initialYear ? this.initialYear : now.getFullYear();
|
||||
this.month = this.initialMonth ? this.initialMonth : now.getMonth() + 1;
|
||||
|
||||
this.calculateIsCurrentMonth();
|
||||
const source = new AddonCalendarMonthSlidesItemsManagerSource(this, moment({
|
||||
year: this.initialYear,
|
||||
month: this.initialMonth ? this.initialMonth - 1 : undefined,
|
||||
}));
|
||||
this.manager = new CoreSwipeSlidesDynamicItemsManager(source);
|
||||
this.managerUnsubscribe = this.manager.addListener({
|
||||
onSelectedItemUpdated: (item) => {
|
||||
this.onMonthViewed(item);
|
||||
},
|
||||
});
|
||||
|
||||
this.fetchData();
|
||||
}
|
||||
|
@ -141,17 +139,31 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
* Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
|
||||
*/
|
||||
ngDoCheck(): void {
|
||||
this.canNavigate = typeof this.canNavigate == 'undefined' ? true : CoreUtils.isTrueOrOne(this.canNavigate);
|
||||
this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
|
||||
CoreUtils.isTrueOrOne(this.displayNavButtons);
|
||||
const items = this.manager?.getSource().getItems();
|
||||
|
||||
if (this.weeks) {
|
||||
if (items?.length) {
|
||||
// Check if there's any change in the filter object.
|
||||
const changes = this.differ.diff(this.filter!);
|
||||
const changes = this.filterDiffer.diff(this.filter ?? {});
|
||||
if (changes) {
|
||||
this.filterEvents();
|
||||
items.forEach((month) => {
|
||||
if (month.loaded && month.weeks) {
|
||||
this.manager?.getSource().filterEvents(month.weeks, this.filter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hiddenDiffer !== this.hidden) {
|
||||
this.hiddenDiffer = this.hidden;
|
||||
|
||||
if (!this.hidden) {
|
||||
this.slides?.slides?.getSwiper().then(swipper => swipper.update());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get timeFormat(): string {
|
||||
return this.manager?.getSource().timeFormat || 'core.strftimetime';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,41 +172,10 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
async fetchData(): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(this.loadCategories());
|
||||
|
||||
// Get offline events.
|
||||
promises.push(AddonCalendarOffline.getAllEditedEvents().then((events) => {
|
||||
// Classify them by month.
|
||||
this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events);
|
||||
|
||||
// Get the IDs of events edited in offline.
|
||||
const filtered = events.filter((event) => event.id! > 0);
|
||||
this.offlineEditedEventsIds = filtered.map((event) => event.id!);
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
// Get events deleted in offline.
|
||||
promises.push(AddonCalendarOffline.getAllDeletedEventsIds().then((ids) => {
|
||||
this.deletedEvents = ids;
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
// Get time format to use.
|
||||
promises.push(AddonCalendar.getCalendarTimeFormat().then((value) => {
|
||||
this.timeFormat = value;
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
|
||||
await this.fetchEvents();
|
||||
await this.manager?.getSource().fetchData();
|
||||
|
||||
await this.manager?.getSource().load(this.manager?.getSelectedItem());
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||
}
|
||||
|
@ -203,113 +184,16 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetch the events for current month.
|
||||
* Update data related to month being viewed.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
* @param month Month being viewed.
|
||||
*/
|
||||
async fetchEvents(): Promise<void> {
|
||||
// Don't pass courseId and categoryId, we'll filter them locally.
|
||||
let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
|
||||
try {
|
||||
result = await AddonCalendar.getMonthlyEvents(this.year!, this.month!);
|
||||
} catch (error) {
|
||||
if (!CoreApp.isOnline()) {
|
||||
// Allow navigating to non-cached months in offline (behave as if using emergency cache).
|
||||
result = await AddonCalendarHelper.getOfflineMonthWeeks(this.year!, this.month!);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
onMonthViewed(month: MonthBasicData): void {
|
||||
// Calculate the period name. We don't use the one in result because it's in server's language.
|
||||
this.periodName = CoreTimeUtils.userDate(
|
||||
new Date(this.year!, this.month! - 1).getTime(),
|
||||
month.moment.unix() * 1000,
|
||||
'core.strftimemonthyear',
|
||||
);
|
||||
this.weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
|
||||
this.weeks = result.weeks as AddonCalendarWeek[];
|
||||
this.calculateIsCurrentMonth();
|
||||
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
day.periodName = CoreTimeUtils.userDate(
|
||||
new Date(this.year!, this.month! - 1, day.mday).getTime(),
|
||||
'core.strftimedaydate',
|
||||
);
|
||||
day.eventsFormated = day.eventsFormated || [];
|
||||
day.filteredEvents = day.filteredEvents || [];
|
||||
day.events.forEach((event) => {
|
||||
/// Format online events.
|
||||
day.eventsFormated!.push(AddonCalendarHelper.formatEventData(event));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (this.isCurrentMonth) {
|
||||
const currentDay = new Date().getDate();
|
||||
let isPast = true;
|
||||
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
day.istoday = day.mday == currentDay;
|
||||
day.ispast = isPast && !day.istoday;
|
||||
isPast = day.ispast;
|
||||
|
||||
if (day.istoday) {
|
||||
day.eventsFormated!.forEach((event) => {
|
||||
event.ispast = this.isEventPast(event);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
// Merge the online events with offline data.
|
||||
this.mergeEvents();
|
||||
// Filter events by course.
|
||||
this.filterEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load categories to be able to filter events.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadCategories(): Promise<void> {
|
||||
if (this.categoriesRetrieved) {
|
||||
// Already retrieved, stop.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cats = await CoreCourses.getCategories(0, true);
|
||||
this.categoriesRetrieved = true;
|
||||
this.categories = {};
|
||||
|
||||
// Index categories by ID.
|
||||
cats.forEach((category) => {
|
||||
this.categories[category.id] = category;
|
||||
});
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter events based on the filter popover.
|
||||
*/
|
||||
filterEvents(): void {
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
|
||||
day.eventsFormated!,
|
||||
this.filter!,
|
||||
this.categories,
|
||||
);
|
||||
|
||||
// Re-calculate some properties.
|
||||
AddonCalendarHelper.calculateDayData(day, day.filteredEvents);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -318,55 +202,30 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
* @param afterChange Whether the refresh is done after an event has changed or has been synced.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async refreshData(afterChange?: boolean): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
async refreshData(afterChange = false): Promise<void> {
|
||||
const selectedMonth = this.manager?.getSelectedItem() || null;
|
||||
|
||||
// Don't invalidate monthly events after a change, it has already been handled.
|
||||
if (!afterChange) {
|
||||
promises.push(AddonCalendar.invalidateMonthlyEvents(this.year!, this.month!));
|
||||
if (afterChange) {
|
||||
this.manager?.getSource().markAllItemsDirty();
|
||||
}
|
||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||
|
||||
this.categoriesRetrieved = false; // Get categories again.
|
||||
await this.manager?.getSource().invalidateContent(selectedMonth);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.fetchData();
|
||||
await this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load next month.
|
||||
*/
|
||||
async loadNext(): Promise<void> {
|
||||
this.increaseMonth();
|
||||
|
||||
this.loaded = false;
|
||||
|
||||
try {
|
||||
await this.fetchEvents();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||
this.decreaseMonth();
|
||||
}
|
||||
this.loaded = true;
|
||||
loadNext(): void {
|
||||
this.slides?.slideNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load previous month.
|
||||
*/
|
||||
async loadPrevious(): Promise<void> {
|
||||
this.decreaseMonth();
|
||||
|
||||
this.loaded = false;
|
||||
|
||||
try {
|
||||
await this.fetchEvents();
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||
this.increaseMonth();
|
||||
}
|
||||
this.loaded = true;
|
||||
loadPrevious(): void {
|
||||
this.slides?.slidePrev();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -386,106 +245,53 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
* @param day Day.
|
||||
*/
|
||||
dayClicked(day: number): void {
|
||||
this.onDayClicked.emit({ day: day, month: this.month!, year: this.year! });
|
||||
}
|
||||
const selectedMonth = this.manager?.getSelectedItem();
|
||||
if (!selectedMonth) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is viewing the current month.
|
||||
*/
|
||||
calculateIsCurrentMonth(): void {
|
||||
const now = new Date();
|
||||
|
||||
this.currentTime = CoreTimeUtils.timestamp();
|
||||
|
||||
this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1;
|
||||
this.isPastMonth = this.year! < now.getFullYear() || (this.year == now.getFullYear() && this.month! < now.getMonth() + 1);
|
||||
this.onDayClicked.emit({ day: day, month: selectedMonth.moment.month() + 1, year: selectedMonth.moment.year() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to current month.
|
||||
*/
|
||||
async goToCurrentMonth(): Promise<void> {
|
||||
const now = new Date();
|
||||
const initialMonth = this.month;
|
||||
const initialYear = this.year;
|
||||
|
||||
this.month = now.getMonth() + 1;
|
||||
this.year = now.getFullYear();
|
||||
const manager = this.manager;
|
||||
const slides = this.slides;
|
||||
if (!manager || !slides) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentMonth = {
|
||||
moment: moment(),
|
||||
};
|
||||
this.loaded = false;
|
||||
|
||||
try {
|
||||
await this.fetchEvents();
|
||||
this.isCurrentMonth = true;
|
||||
// Make sure the day is loaded.
|
||||
await manager.getSource().loadItem(currentMonth);
|
||||
|
||||
slides.slideToItem(currentMonth);
|
||||
} catch (error) {
|
||||
CoreDomUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
|
||||
this.year = initialYear;
|
||||
this.month = initialMonth;
|
||||
}
|
||||
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrease the current month.
|
||||
*/
|
||||
protected decreaseMonth(): void {
|
||||
if (this.month === 1) {
|
||||
this.month = 12;
|
||||
this.year!--;
|
||||
} else {
|
||||
this.month!--;
|
||||
} finally {
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increase the current month.
|
||||
* Check whether selected month is loaded.
|
||||
*/
|
||||
protected increaseMonth(): void {
|
||||
if (this.month === 12) {
|
||||
this.month = 1;
|
||||
this.year!++;
|
||||
} else {
|
||||
this.month!++;
|
||||
}
|
||||
selectedMonthLoaded(): boolean {
|
||||
return !!this.manager?.getSelectedItem()?.loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge online events with the offline events of that period.
|
||||
* Check whether selected month is current month.
|
||||
*/
|
||||
protected mergeEvents(): void {
|
||||
const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } =
|
||||
this.offlineEvents[AddonCalendarHelper.getMonthId(this.year!, this.month!)];
|
||||
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
|
||||
// Schedule notifications for the events retrieved (only future events will be scheduled).
|
||||
AddonCalendar.scheduleEventsNotifications(day.eventsFormated!);
|
||||
|
||||
if (monthOfflineEvents || this.deletedEvents.length) {
|
||||
// There is offline data, merge it.
|
||||
|
||||
if (this.deletedEvents.length) {
|
||||
// Mark as deleted the events that were deleted in offline.
|
||||
day.eventsFormated!.forEach((event) => {
|
||||
event.deleted = this.deletedEvents.indexOf(event.id) != -1;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.offlineEditedEventsIds.length) {
|
||||
// Remove the online events that were modified in offline.
|
||||
day.events = day.events.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1);
|
||||
}
|
||||
|
||||
if (monthOfflineEvents && monthOfflineEvents[day.mday]) {
|
||||
// Add the offline events (either new or edited).
|
||||
day.eventsFormated =
|
||||
AddonCalendarHelper.sortEvents(day.eventsFormated!.concat(monthOfflineEvents[day.mday]));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
selectedMonthIsCurrent(): boolean {
|
||||
return !!this.manager?.getSelectedItem()?.isCurrentMonth;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -493,17 +299,293 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
*
|
||||
* @param eventId Event ID.
|
||||
*/
|
||||
protected undeleteEvent(eventId: number): void {
|
||||
if (!this.weeks) {
|
||||
return;
|
||||
}
|
||||
protected async undeleteEvent(eventId: number): Promise<void> {
|
||||
this.manager?.getSource().getItems()?.some((month) => {
|
||||
if (!month.loaded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
const event = day.eventsFormated!.find((event) => event.id == eventId);
|
||||
return month.weeks?.some((week) => week.days.some((day) => {
|
||||
const event = day.eventsFormated?.find((event) => event.id == eventId);
|
||||
|
||||
if (event) {
|
||||
event.deleted = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.undeleteEventObserver?.off();
|
||||
this.manager?.destroy();
|
||||
this.managerUnsubscribe && this.managerUnsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic data to identify a month.
|
||||
*/
|
||||
type MonthBasicData = {
|
||||
moment: moment.Moment;
|
||||
};
|
||||
|
||||
/**
|
||||
* Preloaded month.
|
||||
*/
|
||||
type PreloadedMonth = MonthBasicData & CoreSwipeSlidesDynamicItem & {
|
||||
weekDays?: AddonCalendarWeekDaysTranslationKeys[];
|
||||
weeks?: AddonCalendarWeek[];
|
||||
isCurrentMonth?: boolean;
|
||||
isPastMonth?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to manage swiping within months.
|
||||
*/
|
||||
class AddonCalendarMonthSlidesItemsManagerSource extends CoreSwipeSlidesDynamicItemsManagerSource<PreloadedMonth> {
|
||||
|
||||
categories?: { [id: number]: CoreCategoryData };
|
||||
// Offline events classified in month & day.
|
||||
offlineEvents: { [monthId: string]: { [day: number]: AddonCalendarEventToDisplay[] } } = {};
|
||||
offlineEditedEventsIds: number[] = []; // IDs of events edited in offline.
|
||||
deletedEvents: number[] = []; // Events deleted in offline.
|
||||
timeFormat?: string;
|
||||
|
||||
protected calendarComponent: AddonCalendarCalendarComponent;
|
||||
|
||||
constructor(component: AddonCalendarCalendarComponent, initialMoment: moment.Moment) {
|
||||
super({ moment: initialMoment });
|
||||
|
||||
this.calendarComponent = component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async fetchData(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.loadCategories(),
|
||||
this.loadOfflineEvents(),
|
||||
this.loadOfflineDeletedEvents(),
|
||||
this.loadTimeFormat(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter events based on the filter popover.
|
||||
*
|
||||
* @param weeks Weeks with the events to filter.
|
||||
* @param filter Filter to apply.
|
||||
*/
|
||||
filterEvents(weeks: AddonCalendarWeek[], filter?: AddonCalendarFilter): void {
|
||||
weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
day.filteredEvents = AddonCalendarHelper.getFilteredEvents(
|
||||
day.eventsFormated || [],
|
||||
filter,
|
||||
this.categories || {},
|
||||
);
|
||||
|
||||
// Re-calculate some properties.
|
||||
AddonCalendarHelper.calculateDayData(day, day.filteredEvents);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load categories to be able to filter events.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadCategories(): Promise<void> {
|
||||
if (this.categories) {
|
||||
// Already retrieved, stop.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const categories = await CoreCourses.getCategories(0, true);
|
||||
|
||||
// Index categories by ID.
|
||||
this.categories = CoreUtils.arrayToObject(categories, 'id');
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load events created or edited in offline.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadOfflineEvents(): Promise<void> {
|
||||
// Get offline events.
|
||||
const events = await AddonCalendarOffline.getAllEditedEvents();
|
||||
|
||||
// Classify them by month.
|
||||
this.offlineEvents = AddonCalendarHelper.classifyIntoMonths(events);
|
||||
|
||||
// Get the IDs of events edited in offline.
|
||||
this.offlineEditedEventsIds = events.filter((event) => event.id > 0).map((event) => event.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load events deleted in offline.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadOfflineDeletedEvents(): Promise<void> {
|
||||
this.deletedEvents = await AddonCalendarOffline.getAllDeletedEventsIds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load time format.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadTimeFormat(): Promise<void> {
|
||||
this.timeFormat = await AddonCalendar.getCalendarTimeFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getItemId(item: MonthBasicData): string | number {
|
||||
return AddonCalendarHelper.getMonthId(item.moment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getPreviousItem(item: MonthBasicData): MonthBasicData | null {
|
||||
return {
|
||||
moment: item.moment.clone().subtract(1, 'month'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getNextItem(item: MonthBasicData): MonthBasicData | null {
|
||||
return {
|
||||
moment: item.moment.clone().add(1, 'month'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async loadItemData(month: MonthBasicData, preload = false): Promise<PreloadedMonth | null> {
|
||||
// Load or preload the weeks.
|
||||
let result: { daynames: Partial<AddonCalendarDayName>[]; weeks: Partial<AddonCalendarWeek>[] };
|
||||
const year = month.moment.year();
|
||||
const monthNumber = month.moment.month() + 1;
|
||||
|
||||
if (preload) {
|
||||
result = await AddonCalendarHelper.getOfflineMonthWeeks(year, monthNumber);
|
||||
} else {
|
||||
try {
|
||||
// Don't pass courseId and categoryId, we'll filter them locally.
|
||||
result = await AddonCalendar.getMonthlyEvents(year, monthNumber);
|
||||
} catch (error) {
|
||||
if (!CoreApp.isOnline()) {
|
||||
// Allow navigating to non-cached months in offline (behave as if using emergency cache).
|
||||
result = await AddonCalendarHelper.getOfflineMonthWeeks(year, monthNumber);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const weekDays = AddonCalendar.getWeekDays(result.daynames[0].dayno);
|
||||
const weeks = result.weeks as AddonCalendarWeek[];
|
||||
const currentDay = new Date().getDate();
|
||||
const currentTime = CoreTimeUtils.timestamp();
|
||||
|
||||
const preloadedMonth: PreloadedMonth = {
|
||||
...month,
|
||||
weeks,
|
||||
weekDays,
|
||||
isCurrentMonth: month.moment.isSame(moment(), 'month'),
|
||||
isPastMonth: month.moment.isBefore(moment(), 'month'),
|
||||
};
|
||||
|
||||
await Promise.all(weeks.map(async (week) => {
|
||||
await Promise.all(week.days.map(async (day) => {
|
||||
day.periodName = CoreTimeUtils.userDate(
|
||||
month.moment.unix() * 1000,
|
||||
'core.strftimedaydate',
|
||||
);
|
||||
day.eventsFormated = day.eventsFormated || [];
|
||||
day.filteredEvents = day.filteredEvents || [];
|
||||
// Format online events.
|
||||
const onlineEventsFormatted = await Promise.all(
|
||||
day.events.map(async (event) => AddonCalendarHelper.formatEventData(event)),
|
||||
);
|
||||
|
||||
day.eventsFormated = day.eventsFormated.concat(onlineEventsFormatted);
|
||||
|
||||
if (preloadedMonth.isCurrentMonth) {
|
||||
day.istoday = day.mday == currentDay;
|
||||
day.ispast = preloadedMonth.isPastMonth || day.mday < currentDay;
|
||||
|
||||
if (day.istoday) {
|
||||
day.eventsFormated?.forEach((event) => {
|
||||
event.ispast = this.isEventPast(event, currentTime);
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
}));
|
||||
|
||||
if (!preload) {
|
||||
// Merge the online events with offline data.
|
||||
this.mergeEvents(month, weeks);
|
||||
// Filter events by course.
|
||||
this.filterEvents(weeks, this.calendarComponent.filter);
|
||||
}
|
||||
|
||||
return preloadedMonth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge online events with the offline events of that period.
|
||||
*
|
||||
* @param month Month.
|
||||
* @param weeks Weeks with the events to filter.
|
||||
*/
|
||||
mergeEvents(month: MonthBasicData, weeks: AddonCalendarWeek[]): void {
|
||||
const monthOfflineEvents: { [day: number]: AddonCalendarEventToDisplay[] } =
|
||||
this.offlineEvents[AddonCalendarHelper.getMonthId(month.moment)];
|
||||
|
||||
weeks.forEach((week) => {
|
||||
week.days.forEach((day) => {
|
||||
if (this.deletedEvents.length) {
|
||||
// Mark as deleted the events that were deleted in offline.
|
||||
day.eventsFormated?.forEach((event) => {
|
||||
event.deleted = this.deletedEvents.indexOf(event.id) != -1;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.offlineEditedEventsIds.length) {
|
||||
// Remove the online events that were modified in offline.
|
||||
day.events = day.events.filter((event) => this.offlineEditedEventsIds.indexOf(event.id) == -1);
|
||||
}
|
||||
|
||||
if (monthOfflineEvents && monthOfflineEvents[day.mday] && day.eventsFormated) {
|
||||
// Add the offline events (either new or edited).
|
||||
day.eventsFormated =
|
||||
AddonCalendarHelper.sortEvents(day.eventsFormated.concat(monthOfflineEvents[day.mday]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -513,18 +595,35 @@ export class AddonCalendarCalendarComponent implements OnInit, DoCheck, OnDestro
|
|||
* Returns if the event is in the past or not.
|
||||
*
|
||||
* @param event Event object.
|
||||
* @param currentTime Current time.
|
||||
* @return True if it's in the past.
|
||||
*/
|
||||
protected isEventPast(event: { timestart: number; timeduration: number}): boolean {
|
||||
return (event.timestart + event.timeduration) < this.currentTime!;
|
||||
isEventPast(event: { timestart: number; timeduration: number}, currentTime: number): boolean {
|
||||
return (event.timestart + event.timeduration) < currentTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
* Invalidate content.
|
||||
*
|
||||
* @param selectedMonth The current selected month.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.undeleteEventObserver?.off();
|
||||
this.obsDefaultTimeChange?.off();
|
||||
async invalidateContent(selectedMonth: PreloadedMonth | null): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (selectedMonth) {
|
||||
promises.push(AddonCalendar.invalidateMonthlyEvents(selectedMonth.moment.year(), selectedMonth.moment.month() + 1));
|
||||
}
|
||||
promises.push(CoreCourses.invalidateCategories(0, true));
|
||||
promises.push(AddonCalendar.invalidateTimeFormat());
|
||||
|
||||
this.categories = undefined; // Get categories again.
|
||||
|
||||
if (selectedMonth) {
|
||||
selectedMonth.dirty = true;
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,13 +18,15 @@ import { CoreSharedModule } from '@/core/shared.module';
|
|||
|
||||
import { AddonCalendarCalendarComponent } from './calendar/calendar';
|
||||
import { AddonCalendarUpcomingEventsComponent } from './upcoming-events/upcoming-events';
|
||||
import { AddonCalendarFilterPopoverComponent } from './filter/filter';
|
||||
import { AddonCalendarFilterComponent } from './filter/filter';
|
||||
import { AddonCalendarReminderTimeModalComponent } from './reminder-time-modal/reminder-time-modal';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonCalendarCalendarComponent,
|
||||
AddonCalendarUpcomingEventsComponent,
|
||||
AddonCalendarFilterPopoverComponent,
|
||||
AddonCalendarFilterComponent,
|
||||
AddonCalendarReminderTimeModalComponent,
|
||||
],
|
||||
imports: [
|
||||
CoreSharedModule,
|
||||
|
@ -34,7 +36,8 @@ import { AddonCalendarFilterPopoverComponent } from './filter/filter';
|
|||
exports: [
|
||||
AddonCalendarCalendarComponent,
|
||||
AddonCalendarUpcomingEventsComponent,
|
||||
AddonCalendarFilterPopoverComponent,
|
||||
AddonCalendarFilterComponent,
|
||||
AddonCalendarReminderTimeModalComponent,
|
||||
],
|
||||
})
|
||||
export class AddonCalendarComponentsModule {}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<ion-list>
|
||||
<ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]">
|
||||
<ion-icon [name]="typeIcons[type]" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'addon.calendar.' + type + 'events' | translate}}</ion-label>
|
||||
<ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()" slot="end"></ion-toggle>
|
||||
</ion-item>
|
||||
<core-spacer *ngIf="filter.course || filter.category || filter.group"></core-spacer>
|
||||
<ng-container *ngIf="filter.course || filter.category || filter.group">
|
||||
<ion-radio-group [(ngModel)]="courseId" (ionChange)="onChange()">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let course of courses">
|
||||
<ion-label><core-format-text [text]="course.fullname"></core-format-text></ion-label>
|
||||
<ion-radio slot="end" [value]="course.id"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ng-container>
|
||||
</ion-list>
|
|
@ -1,21 +0,0 @@
|
|||
:host {
|
||||
ion-item {
|
||||
ion-icon, ion-radio {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> ion-icon {
|
||||
padding: 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context([dir=rtl]) {
|
||||
ion-item {
|
||||
ion-icon, ion-radio {
|
||||
margin-left: 8px;
|
||||
margin-right: unset;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<ion-header class="no-title">
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="fas-times" slot="icon-only" aria-hidden=true></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content [fullscreen]="true">
|
||||
<ion-list>
|
||||
<ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]">
|
||||
<ion-icon [name]="typeIcons[type]" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'addon.calendar.' + type + 'events' | translate}}</ion-label>
|
||||
<ion-toggle [(ngModel)]="filter[type]" (ionChange)="onChange()" slot="end"></ion-toggle>
|
||||
</ion-item>
|
||||
<core-spacer *ngIf="filter.course || filter.category || filter.group"></core-spacer>
|
||||
<ng-container *ngIf="filter.course || filter.category || filter.group">
|
||||
<ion-radio-group [(ngModel)]="courseId" (ionChange)="onChange()">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let course of courses">
|
||||
<ion-label>
|
||||
<core-format-text [text]="course.fullname"></core-format-text>
|
||||
</ion-label>
|
||||
<ion-radio slot="end" [value]="course.id"></ion-radio>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
</ion-content>
|
|
@ -0,0 +1,14 @@
|
|||
@import "~theme/globals";
|
||||
|
||||
:host {
|
||||
ion-item {
|
||||
ion-icon, ion-radio {
|
||||
@include margin-horizontal(null, 8px);
|
||||
}
|
||||
|
||||
> ion-icon {
|
||||
padding: 4px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { CoreEnrolledCourseData } from '@features/courses/services/courses';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ModalController } from '@singletons';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { AddonCalendarEventType, AddonCalendarProvider } from '../../services/calendar';
|
||||
import { AddonCalendarFilter, AddonCalendarEventIcons } from '../../services/calendar-helper';
|
||||
|
@ -23,11 +24,11 @@ import { AddonCalendarFilter, AddonCalendarEventIcons } from '../../services/cal
|
|||
* Component to display the events filter that includes events types and a list of courses.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-calendar-filter-popover',
|
||||
templateUrl: 'addon-calendar-filter-popover.html',
|
||||
styleUrls: ['../../calendar-common.scss', 'filter-popover.scss'],
|
||||
selector: 'addon-calendar-filter',
|
||||
templateUrl: 'filter.html',
|
||||
styleUrls: ['../../calendar-common.scss', 'filter.scss'],
|
||||
})
|
||||
export class AddonCalendarFilterPopoverComponent implements OnInit {
|
||||
export class AddonCalendarFilterComponent implements OnInit {
|
||||
|
||||
@Input() filter: AddonCalendarFilter = {
|
||||
filtered: false,
|
||||
|
@ -56,7 +57,7 @@ export class AddonCalendarFilterPopoverComponent implements OnInit {
|
|||
}
|
||||
|
||||
/**
|
||||
* Init the component.
|
||||
* @inheritdoc
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.courseId = this.filter.courseId || -1;
|
||||
|
@ -80,4 +81,11 @@ export class AddonCalendarFilterPopoverComponent implements OnInit {
|
|||
CoreEvents.trigger(AddonCalendarProvider.FILTER_CHANGED_EVENT, this.filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
<h2>{{ 'addon.calendar.reminders' | translate }}</h2>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button fill="clear" (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-times" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<form (ngSubmit)="saveReminder()">
|
||||
<ion-radio-group name="radiovalue" [(ngModel)]="radioValue" class="ion-text-wrap">
|
||||
<!-- Preset options. -->
|
||||
<ion-item *ngIf="allowDisable">
|
||||
<ion-label>
|
||||
<p>{{ 'core.settings.disabled' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-radio slot="end" value="disabled"></ion-radio>
|
||||
</ion-item>
|
||||
<ion-item *ngFor="let option of presetOptions">
|
||||
<ion-label>
|
||||
<p>{{ option.label }}</p>
|
||||
</ion-label>
|
||||
<ion-radio slot="end" [value]="option.radioValue"></ion-radio>
|
||||
</ion-item>
|
||||
|
||||
<!-- Custom value. -->
|
||||
<ion-item class="ion-text-wrap">
|
||||
<ion-label>
|
||||
<p>{{ 'core.custom' | translate }}</p>
|
||||
</ion-label>
|
||||
<ion-radio slot="end" value="custom"></ion-radio>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" (click)="customInputClicked($event)">
|
||||
<ion-label></ion-label>
|
||||
|
||||
<div class="flex-row">
|
||||
<!-- Input to enter the value. -->
|
||||
<ion-input type="number" name="customvalue" [(ngModel)]="customValue" [disabled]="radioValue != 'custom'"
|
||||
placeholder="10" (click)="customInputClicked($event)">
|
||||
</ion-input>
|
||||
|
||||
<!-- Units. -->
|
||||
<label class="accesshide" for="reminderUnits">{{ 'addon.calendar.units' | translate }}</label>
|
||||
<ion-select id="reminderUnits" name="customunits" [(ngModel)]="customUnits" interface="action-sheet"
|
||||
[disabled]="radioValue != 'custom'" slot="end" [interfaceOptions]="{header: 'addon.calendar.units' | translate}">
|
||||
<ion-select-option *ngFor="let option of customUnitsOptions" [value]="option.value">
|
||||
{{ option.label | translate }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</div>
|
||||
</ion-item>
|
||||
</ion-radio-group>
|
||||
|
||||
<ion-button type="submit" class="ion-margin" expand="block" [disabled]="radioValue == 'custom' && !customValue">
|
||||
{{ 'core.done' | translate }}
|
||||
</ion-button>
|
||||
</form>
|
||||
</ion-content>
|
|
@ -0,0 +1,174 @@
|
|||
// (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 { AddonCalendar, AddonCalendarReminderUnits, AddonCalendarValueAndUnit } from '@addons/calendar/services/calendar';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { ModalController } from '@singletons';
|
||||
|
||||
/**
|
||||
* Modal to choose a reminder time.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-calendar-new-reminder-modal',
|
||||
templateUrl: 'reminder-time-modal.html',
|
||||
})
|
||||
export class AddonCalendarReminderTimeModalComponent implements OnInit {
|
||||
|
||||
@Input() initialValue?: AddonCalendarValueAndUnit;
|
||||
@Input() allowDisable?: boolean;
|
||||
|
||||
radioValue = '5m';
|
||||
customValue = '10';
|
||||
customUnits = AddonCalendarReminderUnits.MINUTE;
|
||||
|
||||
presetOptions = [
|
||||
{
|
||||
radioValue: '5m',
|
||||
value: 5,
|
||||
unit: AddonCalendarReminderUnits.MINUTE,
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
radioValue: '10m',
|
||||
value: 10,
|
||||
unit: AddonCalendarReminderUnits.MINUTE,
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
radioValue: '30m',
|
||||
value: 30,
|
||||
unit: AddonCalendarReminderUnits.MINUTE,
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
radioValue: '1h',
|
||||
value: 1,
|
||||
unit: AddonCalendarReminderUnits.HOUR,
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
radioValue: '12h',
|
||||
value: 12,
|
||||
unit: AddonCalendarReminderUnits.HOUR,
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
radioValue: '1d',
|
||||
value: 1,
|
||||
unit: AddonCalendarReminderUnits.DAY,
|
||||
label: '',
|
||||
},
|
||||
];
|
||||
|
||||
customUnitsOptions = [
|
||||
{
|
||||
value: AddonCalendarReminderUnits.MINUTE,
|
||||
label: 'core.minutes',
|
||||
},
|
||||
{
|
||||
value: AddonCalendarReminderUnits.HOUR,
|
||||
label: 'core.hours',
|
||||
},
|
||||
{
|
||||
value: AddonCalendarReminderUnits.DAY,
|
||||
label: 'core.days',
|
||||
},
|
||||
{
|
||||
value: AddonCalendarReminderUnits.WEEK,
|
||||
label: 'core.weeks',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.presetOptions.forEach((option) => {
|
||||
option.label = AddonCalendar.getUnitValueLabel(option.value, option.unit);
|
||||
});
|
||||
|
||||
if (!this.initialValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.initialValue.value === 0) {
|
||||
this.radioValue = 'disabled';
|
||||
} else {
|
||||
// Search if it's one of the preset options.
|
||||
const option = this.presetOptions.find(option =>
|
||||
option.value === this.initialValue?.value && option.unit === this.initialValue.unit);
|
||||
|
||||
if (option) {
|
||||
this.radioValue = option.radioValue;
|
||||
} else {
|
||||
// It's a custom value.
|
||||
this.radioValue = 'custom';
|
||||
this.customValue = String(this.initialValue.value);
|
||||
this.customUnits = this.initialValue.unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal.
|
||||
*/
|
||||
closeModal(): void {
|
||||
ModalController.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the reminder.
|
||||
*/
|
||||
saveReminder(): void {
|
||||
if (this.radioValue === 'disabled') {
|
||||
ModalController.dismiss(0);
|
||||
} else if (this.radioValue === 'custom') {
|
||||
const value = parseInt(this.customValue, 10);
|
||||
if (!value) {
|
||||
CoreDomUtils.showErrorModal('core.errorinvalidform', true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ModalController.dismiss(Math.abs(value) * this.customUnits);
|
||||
} else {
|
||||
const option = this.presetOptions.find(option => option.radioValue === this.radioValue);
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModalController.dismiss(option.unit * option.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom value input clicked.
|
||||
*
|
||||
* @param ev Click event.
|
||||
*/
|
||||
async customInputClicked(ev: Event): Promise<void> {
|
||||
if (this.radioValue === 'custom') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.radioValue = 'custom';
|
||||
|
||||
const target = <HTMLInputElement | HTMLElement | null> ev.target;
|
||||
if (target) {
|
||||
CoreDomUtils.focusElement(target);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue