diff --git a/.gitignore b/.gitignore index de9eaa33e..be5a2b54d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,13 @@ platforms/ /plugins/ /plugins/android.json /plugins/ios.json +resources/android/icon +resources/android/splash +resources/ios/icon +resources/ios/splash +resources/windows/icon +resources/windows/splash +config.xml www/ !www/README.md $RECYCLE.BIN/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..4fd021952 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 60d343646..f7dfb6833 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,125 @@ os: linux dist: bionic group: edge - language: node_js node_js: 11 +php: 7.1 + +android: + components: + - tools + - platform-tools + - build-tools-29.0.3 + - android-28 + - extra-google-google_play_services + - extra-google-m2repository + - extra-android-m2repository + +git: + depth: 3 before_cache: - rm -rf $HOME/.cache/electron-builder/wine + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ cache: directories: - $HOME/.npm - $HOME/.cache/electron - $HOME/.cache/electron-builder + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + - $HOME/.android/build-cache before_script: - - npm install npm@latest -g + - if [ "$TRAVIS_OS_NAME" != 'windows' ] ; then npm install npm@latest -g ; fi - gulp -script: - - npm run build --bailOnLintError true --typeCheckOnLint true - -after_success: - - scripts/ci.sh +jobs: + include: + - stage: check + if: NOT branch =~ /(master|integration|desktop)$/ AND env(DEPLOY) IS blank + os: linux + script: npm run build --bailOnLintError true --typeCheckOnLint true + - stage: mirror + if: branch IN (master, integration, desktop) AND repo = moodlehq/moodleapp AND type != cron + os: linux + script: scripts/mirror.sh + - stage: prepare + if: branch =~ /(master|^integration)$/ AND env(PREPARE) IS NOT blank AND env(PREPARE) = 1 AND type != cron + os: linux + script: scripts/aot.sh + - stage: build + name: "Build Android" + if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND NOT branch = desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank)) + os: linux + dist: trusty + group: edge + language: android + env: + - BUILD_PLATFORM='android' + before_install: + - nvm install 11 + - node --version + - npm --version + - nvm --version + - npm ci + - npm install -g gulp + script: scripts/aot.sh + - stage: build + name: "Build iOS" + if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND NOT branch = desktop) OR (env(DEPLOY) IN (2,3) AND tag IS NOT blank)) + os: osx + osx_image: xcode11.3 + env: + - BUILD_PLATFORM='ios' + script: scripts/aot.sh + - stage: build + name: "Build Linux" + if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch = desktop) OR (env(DEPLOY) = 3 AND tag IS NOT blank)) + os: linux + env: + - ELECTRON_CACHE=$HOME/.cache/electron + - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder + - BUILD_PLATFORM='linux' + script: scripts/aot.sh + - stage: build + name: "Build MacOS" + if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch = desktop) OR (env(DEPLOY) = 3 AND tag IS NOT blank)) + os: osx + osx_image: xcode11.3 + env: + - ELECTRON_CACHE=$HOME/.cache/electron + - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder + - BUILD_PLATFORM='osx' + script: scripts/aot.sh + - stage: build + name: "Build Windows" + if: env(DEPLOY) IS NOT blank AND ((env(DEPLOY) = 1 AND branch = desktop) OR (env(DEPLOY) = 3 AND tag IS NOT blank)) + os: windows + env: + - ELECTRON_CACHE=$HOME/.cache/electron + - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder + - ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true + - DEBUG=electron-windows-store + - BUILD_PLATFORM='windows' + script: scripts/aot.sh + - stage: test + name: "End to end tests (mod_forum, mod_messages and mod_comments)" + services: + - docker + if: type = cron + script: scripts/test_e2e.sh "@app&&@mod_forum" "@app&&@mod_messages" "@app&&@mod_comments" + - stage: test + name: "End to end tests (mod_data, mod_survey, mod_course, core_course and mod_courses)" + services: + - docker + if: type = cron + script: scripts/test_e2e.sh "@app&&@mod_data" "@app&&@mod_survey" "@app&&@mod_course" "@app&&@core_course" "@app&&@mod_courses" + - stage: test + name: "End to end tests (others)" + services: + - docker + if: type = cron + script: scripts/test_e2e.sh "@app&&~@mod_forum&&~@mod_messages&&~@mod_comments&&~@mod_data&&~@mod_survey&&~@mod_course&&~@core_course&&~@mod_courses" diff --git a/config.xml b/config.xml index 6f949ffa9..fac066d54 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + Moodle Moodle official app Moodle Mobile team @@ -8,6 +8,7 @@ + @@ -17,179 +18,631 @@ + + - - - + + - - + + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - We need your location so you can attach it as part of your submissions. We need your location so you can attach it as part of your submissions. - - - - - - - - - - - - - - - - - - + + We need camera access to take pictures so you can attach them as part of your submissions. + + + We need microphone access to record sounds so you can attach them as part of your submissions. + + + We need photo library access to get pictures from there so you can attach them as part of your submissions. + + + + + + 3.9.0 + + + YES + + + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + + + + + + + CFBundleTypeName + Unknown File + LSHandlerRank + Alternate + LSItemContentTypes + + public.calendar-event + public.database + public.executable + public.data + public.content + public.item + + + + CFBundleTypeName + Video + LSHandlerRank + Alternate + LSItemContentTypes + + public.video + + + + CFBundleTypeName + Image + LSHandlerRank + Alternate + LSItemContentTypes + + public.image + + + + CFBundleTypeName + Web Archive + LSHandlerRank + Alternate + LSItemContentTypes + + com.apple.webarchive + + + + CFBundleTypeName + iWork Keynote + LSHandlerRank + Alternate + LSItemContentTypes + + com.apple.keynote.key + com.apple.iwork.keynote.key + com.apple.iwork.keynote.kth + + + + CFBundleTypeName + iWork Numbers + LSHandlerRank + Alternate + LSItemContentTypes + + com.apple.numbers.numbers + com.apple.iwork.numbers.numbers + com.apple.iwork.numbers.template + + + + CFBundleTypeName + iWork Pages + LSHandlerRank + Alternate + LSItemContentTypes + + com.apple.page.pages + com.apple.iwork.pages.pages + com.apple.iwork.pages.template + + + + CFBundleTypeName + OpenDocument Spreadsheet + LSHandlerRank + Alternate + LSItemContentTypes + + org.oasis.opendocument.spreadsheet + + + + CFBundleTypeName + OpenDocument Presentation + LSHandlerRank + Alternate + LSItemContentTypes + + org.oasis.opendocument.presentation + + + + CFBundleTypeName + OpenDocument Text + LSHandlerRank + Alternate + LSItemContentTypes + + org.oasis.opendocument.text + + + + CFBundleTypeName + Folder + LSHandlerRank + Alternate + LSItemContentTypes + + public.folder + + + + CFBundleTypeName + Audio + LSHandlerRank + Alternate + LSItemContentTypes + + public.audio + public.mp3 + public.mpeg-4-audio + com.apple.protected-​mpeg-4-audio + public.aifc-audio + com.apple.coreaudio-​format + public.aiff-audio + + + + CFBundleTypeName + Movie + LSHandlerRank + Alternate + LSItemContentTypes + + public.movie + public.3gpp2 + public.3gpp + public.mpeg + com.apple.quicktime-movie + public.mpeg-4 + + + + CFBundleTypeName + GIF image + LSHandlerRank + Alternate + LSItemContentTypes + + com.compuserve.gif + + + + CFBundleTypeName + PNG image + LSHandlerRank + Alternate + LSItemContentTypes + + public.png + + + + CFBundleTypeName + TIFF image + LSHandlerRank + Alternate + LSItemContentTypes + + public.tiff + + + + CFBundleTypeName + JPEG image + LSHandlerRank + Alternate + LSItemContentTypes + + public.jpeg + + + + CFBundleTypeName + XML + LSHandlerRank + Alternate + LSItemContentTypes + + public.xml + + + + CFBundleTypeName + HTML + LSHandlerRank + Alternate + LSItemContentTypes + + public.html + public.xhtml + + + + CFBundleTypeName + Rich Text + LSHandlerRank + Alternate + LSItemContentTypes + + public.rtf + com.apple.rtfd + com.apple.flat-rtfd + + + + CFBundleTypeName + Text + LSHandlerRank + Alternate + LSItemContentTypes + + public.text + public.plain-text + public.utf8-plain-text + public.utf16-external-plain-​text + public.utf16-plain-text + com.apple.traditional-mac-​plain-text + public.source-code + public.c-source + public.objective-c-source + public.c-plus-plus-source + public.objective-c-plus-​plus-source + public.c-header + public.c-plus-plus-header + com.sun.java-source + public.script + public.shell-script + + + + CFBundleTypeExtensions + + zip + zipx + + CFBundleTypeName + Zip archive + LSHandlerRank + Alternate + LSItemContentTypes + + public.zip-archive + public.archive + com.pkware.zip-archive + com.pkware.zipx-archive + + + + CFBundleTypeExtensions + + rar + RAR + + CFBundleTypeName + Rar archive + LSHandlerRank + Alternate + LSItemContentTypes + + com.rarlab.rar-archive + public.archive + + + + CFBundleTypeExtensions + + 7z + 7Z + + CFBundleTypeName + 7z archive + LSHandlerRank + Alternate + LSItemContentTypes + + org.7-zip.7-zip-archive + public.archive + + + + CFBundleTypeName + Waveform audio + LSHandlerRank + Alternate + LSItemContentTypes + + com.microsoft.waveform-​audio + + + + CFBundleTypeName + Windows icon image + LSHandlerRank + Alternate + LSItemContentTypes + + com.microsoft.ico + com.apple.icns + + + + CFBundleTypeName + Windows bitmap image + LSHandlerRank + Alternate + LSItemContentTypes + + com.microsoft.bmp + + + + CFBundleTypeName + Microsoft PowerPoint + LSHandlerRank + Alternate + LSItemContentTypes + + com.microsoft.powerpoint.​ppt + org.openxmlformats.presentationml.presentation + + + + CFBundleTypeName + Microsoft Excel + LSHandlerRank + Alternate + LSItemContentTypes + + org.openxmlformats.spreadsheetml.sheet + com.microsoft.excel.xls + + + + CFBundleTypeName + Microsoft Word + LSHandlerRank + Alternate + LSItemContentTypes + + com.microsoft.word.doc + com.microsoft.word.wordml + org.openxmlformats.wordprocessingml.document + + + + CFBundleTypeName + PDF + LSHandlerRank + Alternate + LSItemContentTypes + + com.adobe.pdf + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - YES - - - diff --git a/config/webpack.config.js b/config/webpack.config.js index 3f622bcd3..1c6ae7ca5 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -12,7 +12,8 @@ const customConfig = { '@providers': resolve('./src/providers'), '@components': resolve('./src/components'), '@directives': resolve('./src/directives'), - '@pipes': resolve('./src/pipes') + '@pipes': resolve('./src/pipes'), + '@singletons': resolve('./src/singletons'), } }, externals: [ diff --git a/desktop/assets/windows/AppXManifest.xml b/desktop/assets/windows/AppXManifest.xml index 583b9fdc1..fc2427ea7 100644 --- a/desktop/assets/windows/AppXManifest.xml +++ b/desktop/assets/windows/AppXManifest.xml @@ -6,7 +6,7 @@ + Version="3.9.0.0" /> Moodle Desktop Moodle Pty Ltd. diff --git a/package-lock.json b/package-lock.json index 88471ab97..7623fa790 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.8.1", + "version": "3.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -107,6 +107,11 @@ "resolved": "https://registry.npmjs.org/@ionic-native/camera/-/camera-4.20.0.tgz", "integrity": "sha512-WnfQq8RV+7ezOqpCyNx9Xgpy7Y8TZehGLSxZXnCqCbFZ72CpC70Q5AV/eTIRGiKkotx2U6nUopYF+gTj1cunFA==" }, + "@ionic-native/chooser": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@ionic-native/chooser/-/chooser-4.20.0.tgz", + "integrity": "sha512-3Qn6TqWBzs2EQV0zp0uoTRDVPPdC/EgkdzIQVnryTZg1p1NZ3L8hKuIOHms/WqFwOGMeA87dMImxSgHjw9JXJQ==" + }, "@ionic-native/clipboard": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@ionic-native/clipboard/-/clipboard-4.20.0.tgz", @@ -147,6 +152,11 @@ "resolved": "https://registry.npmjs.org/@ionic-native/globalization/-/globalization-4.20.0.tgz", "integrity": "sha512-zyxaW+vZb1OHeDgGbrZHQe3hy30K4YeKjGr8KNGcwq+k2ZHkfqo/H6XIwf2m/UlFTgacvdR9XZtfP+6N0suybg==" }, + "@ionic-native/http": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@ionic-native/http/-/http-4.20.0.tgz", + "integrity": "sha512-DF+Y1oYoHTv9Y22a2jLgniOmj9Twba+9j8rzHA4xboVT2HpB6bsBSWOktdAXDVjoajXiLsA/u7fh6YD8//NVGg==" + }, "@ionic-native/in-app-browser": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@ionic-native/in-app-browser/-/in-app-browser-4.20.0.tgz", @@ -177,6 +187,11 @@ "resolved": "https://registry.npmjs.org/@ionic-native/push/-/push-4.20.0.tgz", "integrity": "sha512-IgzaZd8KSPLwyLX1emRijlQ0Vfa3RlPPBx370lVH32c8zG3DFH1xfQQbb39KF3qmX5b6so0pGGA2holSUwVm2w==" }, + "@ionic-native/qr-scanner": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@ionic-native/qr-scanner/-/qr-scanner-4.20.0.tgz", + "integrity": "sha512-eLeJQq49/x5bdCVLotuMHZZ3YGEpSzuEnuX2vno2ugdGSygBm+wxIVSa9Nuz8HozYwC6oyii+zH/pg4SZ+4V9Q==" + }, "@ionic-native/screen-orientation": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@ionic-native/screen-orientation/-/screen-orientation-4.20.0.tgz", @@ -618,6 +633,973 @@ } } }, + "@ionic/cli": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@ionic/cli/-/cli-6.9.3.tgz", + "integrity": "sha512-pTMSFczhjMpPnh8fnxuMGU4tcvPlUYZPambNKZdFzRVNQasK00kqrR/Vc8dlHNNRjB/99Hu+wu3H68/7ooU6ww==", + "dev": true, + "requires": { + "@ionic/cli-framework": "4.1.5", + "@ionic/cli-framework-prompts": "2.1.3", + "@ionic/utils-array": "2.1.3", + "@ionic/utils-fs": "3.1.3", + "@ionic/utils-network": "2.1.3", + "@ionic/utils-process": "2.1.3", + "@ionic/utils-stream": "3.1.3", + "@ionic/utils-subprocess": "2.1.3", + "@ionic/utils-terminal": "2.1.3", + "chalk": "^4.0.0", + "debug": "^4.0.0", + "diff": "^4.0.1", + "elementtree": "^0.1.7", + "leek": "0.0.24", + "lodash": "^4.17.5", + "open": "^7.0.4", + "os-name": "^3.1.0", + "semver": "^7.1.1", + "split2": "^3.0.0", + "ssh-config": "^1.1.1", + "stream-combiner2": "^1.1.1", + "superagent": "^5.2.1", + "superagent-proxy": "^2.0.0", + "tar": "^6.0.1", + "through2": "^3.0.0", + "tslib": "1.11.2", + "uuid": "^7.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "macos-release": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz", + "integrity": "sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==", + "dev": true + }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "os-name": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-3.1.0.tgz", + "integrity": "sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==", + "dev": true, + "requires": { + "macos-release": "^2.2.0", + "windows-release": "^3.1.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tar": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz", + "integrity": "sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "dev": true, + "requires": { + "readable-stream": "2 || 3" + } + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@ionic/cli-framework": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework/-/cli-framework-4.1.5.tgz", + "integrity": "sha512-rrFJQ4hyQYPsl//sF8zLzMwSstHj/OjY3Ac8gTJ7jDETDYus5wfOr4EAEUcnpvaCePPcSI6bTvh1Bpkr04Ng9A==", + "dev": true, + "requires": { + "@ionic/utils-array": "2.1.3", + "@ionic/utils-fs": "3.1.3", + "@ionic/utils-object": "2.1.3", + "@ionic/utils-process": "2.1.3", + "@ionic/utils-stream": "3.1.3", + "@ionic/utils-subprocess": "2.1.3", + "@ionic/utils-terminal": "2.1.3", + "chalk": "^4.0.0", + "debug": "^4.0.0", + "lodash": "^4.17.5", + "log-update": "^4.0.0", + "minimist": "^1.2.0", + "rimraf": "^3.0.0", + "slice-ansi": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "tslib": "1.11.2", + "untildify": "^4.0.0", + "wrap-ansi": "^7.0.0", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + } + } + }, + "@ionic/cli-framework-prompts": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@ionic/cli-framework-prompts/-/cli-framework-prompts-2.1.3.tgz", + "integrity": "sha512-tYjXIkoUdl6SPDuLHsUXbIyXLiwofsqNjkrNhbJ2Ed8oSiBlhfio/XZ1nKAEHaoHpud+UfD5t++goYnGVa4fBw==", + "dev": true, + "requires": { + "@ionic/utils-terminal": "2.1.3", + "debug": "^4.0.0", + "inquirer": "^7.0.0", + "tslib": "1.11.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "inquirer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "rxjs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz", + "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + } + } + }, + "@ionic/utils-array": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.3.tgz", + "integrity": "sha512-IV7oK7kj6UZEkZ5lbS78gNSUSTqZtLOEKu9G+MqBpRTX+YKKnmsAxQuvZrnsy/pHmzJ7aKlj1V0gNAFO6w/NOA==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "1.11.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + } + } + }, + "@ionic/utils-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.3.tgz", + "integrity": "sha512-xMZhlB1XgZQchvKZFQt6NcKgLsCdSTt3lmU0Gl0HIWdGHjwI5QSyLCYPTvfa8Wm4vCn7XYiMy05bWxWtY0+FxQ==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "fs-extra": "^9.0.0", + "through2": "^3.0.0", + "tslib": "1.11.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "fs-extra": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz", + "integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "jsonfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz", + "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^1.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "dev": true, + "requires": { + "readable-stream": "2 || 3" + } + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + }, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "dev": true + } + } + }, + "@ionic/utils-network": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-network/-/utils-network-2.1.3.tgz", + "integrity": "sha512-6R40gzy8vr2CTV/gvq4uTSZrkviR1IEtT1M4T+9KnTO6+tFLg08oilpJrxPvZh4SMr+VdIIus+LSOtdBzI+Dwg==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "1.11.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + } + } + }, + "@ionic/utils-object": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.3.tgz", + "integrity": "sha512-iJQjC2RBWACCgwafsKKJN+G2hxTxRhVT0gtdGK29jH8ZZMIJGEEIA2hlpPsL9OR/yRMwByATyO1usC2jEM8qDQ==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "1.11.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + } + } + }, + "@ionic/utils-process": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.3.tgz", + "integrity": "sha512-FMW9kc+waKv01/dNuMOP3NrJLyhMv8Ij73B2KVlZyI6UiFMjghvEApttQVi2ewKw6z1ipbkSFRaICxPIvGwABw==", + "dev": true, + "requires": { + "@ionic/utils-object": "2.1.3", + "@ionic/utils-terminal": "2.1.3", + "debug": "^4.0.0", + "lodash": "^4.17.5", + "tree-kill": "^1.2.2", + "tslib": "1.11.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + } + } + }, + "@ionic/utils-stream": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.3.tgz", + "integrity": "sha512-vhYVJMT/5HNhTe3ypPkgll4e4O6fRXqAEXntJzBy3COWllXkERB/tWn2x8TSLcosmzN8f8FONCjznnq6Uq54LQ==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "1.11.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + } + } + }, + "@ionic/utils-subprocess": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-2.1.3.tgz", + "integrity": "sha512-jTEz97eFxNLj+Cw8CQvfkNThEBT98wrbPfE2C3MO9E+gw+h7gX2/KWoUgg9U0lAyiOUggCVir/44SLbevhtPBw==", + "dev": true, + "requires": { + "@ionic/utils-array": "2.1.3", + "@ionic/utils-fs": "3.1.3", + "@ionic/utils-process": "2.1.3", + "@ionic/utils-stream": "3.1.3", + "@ionic/utils-terminal": "2.1.3", + "cross-spawn": "^7.0.0", + "debug": "^4.0.0", + "tslib": "1.11.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@ionic/utils-terminal": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.1.3.tgz", + "integrity": "sha512-By0tp8pBcghIqTMKnRQw55cnUhDGrE+UTfdY81iDiXaMxRy8vwJjXgq9jiCKLF/qH1elQpIzDo2ePdu+MrNMFg==", + "dev": true, + "requires": { + "debug": "^4.0.0", + "tslib": "1.11.2" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.2.tgz", + "integrity": "sha512-tTSkux6IGPnUGUd1XAZHcpu85MOkIl5zX49pO+jfsie3eP0B6pyhOlLXm3cAC6T7s+euSDDUUV+Acop5WmtkVg==", + "dev": true + } + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -642,6 +1624,12 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/cordova": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", @@ -746,6 +1734,15 @@ } } }, + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, "ajv": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", @@ -799,9 +1796,9 @@ "dev": true }, "android-versions": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/android-versions/-/android-versions-1.4.0.tgz", - "integrity": "sha512-GnomfYsBq+nZh3c3UH/4r9Jt6FuTxdhUJbeHIdYOH5xBhQ8I0ZzC2/RM5IFFIjrzuNWSHb8JWP1lPK0/a26jrg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/android-versions/-/android-versions-1.5.0.tgz", + "integrity": "sha512-/GWUAqa2OJNlDF5VGSe3SR1QMHEPXxx54Ur56r0qQC0H9FlBr7kyBF2SgVEhzFCPbrW4UcYgVuWrq/2Ty3QvXg==", "requires": { "semver": "^5.4.1" } @@ -1160,6 +2157,18 @@ "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, + "ast-types": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.3.tgz", + "integrity": "sha512-XTZ7xGML849LkQP86sWdQzfhwbt3YwIO6MqbX9mUNYY98VKaaVZP7YNNm70IpwecbkkxmfC5IYAzOQ/2p29zRA==", + "dev": true + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", @@ -1217,6 +2226,12 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", @@ -1366,7 +2381,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -1400,6 +2416,15 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -1697,6 +2722,12 @@ "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", "dev": true }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, "buffer-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", @@ -2304,6 +3335,12 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, + "cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "dev": true + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -2369,9 +3406,9 @@ }, "dependencies": { "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -2620,6 +3657,11 @@ } } }, + "cordova-plugin-advanced-http": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/cordova-plugin-advanced-http/-/cordova-plugin-advanced-http-2.4.1.tgz", + "integrity": "sha512-6G8MTy/d02jE6n3Y9CVyCtD5hZGiBb+/dR2AIzhKN1RGGz38g1D2C8yE4MqHRvnmry6k/KHQWT1MsHNXrjouXQ==" + }, "cordova-plugin-badge": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/cordova-plugin-badge/-/cordova-plugin-badge-0.8.8.tgz", @@ -2630,6 +3672,11 @@ "resolved": "https://registry.npmjs.org/cordova-plugin-camera/-/cordova-plugin-camera-4.1.0.tgz", "integrity": "sha512-fCLhWjWYn49q3X5xaypAPgTz6MAWSKFFQvD2Gpi5SuVlrRPRphtX2jIqR2zCBuDTBR082QVnlc+yUDXt65Mjgw==" }, + "cordova-plugin-chooser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cordova-plugin-chooser/-/cordova-plugin-chooser-1.3.1.tgz", + "integrity": "sha512-xyTgu7T1WSk4XeHVwrez1ZB+iPDThae79OYuuPTJkgHm4fVeD5QzzgJVxo2AETztAOM20OQU6txedfBYB6RHhQ==" + }, "cordova-plugin-customurlscheme": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-5.0.0.tgz", @@ -2656,9 +3703,8 @@ "integrity": "sha1-p12L4uvDu5sjxbG70ZkhTsJnWGs=" }, "cordova-plugin-geolocation": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cordova-plugin-geolocation/-/cordova-plugin-geolocation-4.0.2.tgz", - "integrity": "sha512-QGThnPKzPxESHkruZlpE0+5aFBVOet8al0vIJ7laSUOQHIC1dd/JY6peVIbtLboKi5Dap1wCKRubOqPqH8xcQA==" + "version": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", + "from": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff" }, "cordova-plugin-globalization": { "version": "1.11.0", @@ -2666,15 +3712,20 @@ "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4=" }, "cordova-plugin-inappbrowser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-3.2.0.tgz", - "integrity": "sha512-tYsK0H9M8POmJTVnfyIsiRgoOxnypa9IQIbf/Hsgi7vbgUYRHtBUfvXwq4RhMqLIVrCeJLXF2hTXTDNY0a/eTA==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-4.0.0.tgz", + "integrity": "sha512-w2LZzdF3R4G/EqVZ9aWch9Pksk76uw6/S5wFP1sgn7zjsSDpJBb/JhazLnioN1NZmZiCUBbROv1S4+9JCkeCgA==" }, "cordova-plugin-ionic-keyboard": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.1.3.tgz", "integrity": "sha512-6ucQ6FdlLdBm8kJfFnzozmBTjru/0xekHP/dAhjoCZggkGRlgs8TsUJFkxa/bV+qi7Dlo50JjmpE4UMWAO+aOQ==" }, + "cordova-plugin-ionic-webview": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-webview/-/cordova-plugin-ionic-webview-4.1.3.tgz", + "integrity": "sha512-hlrUF0kLjjEkZmpYlLJO0NnXmVjMmQ3MOZVXm1ytDihLPKHklYCOpCvjA5Wz3hJrPD1shFEsqi/SPnp873AsdQ==" + }, "cordova-plugin-local-notification": { "version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#0bb96b757fb484553ceabf35a59802f7983a2836", "from": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle" @@ -2689,6 +3740,14 @@ "resolved": "https://registry.npmjs.org/cordova-plugin-network-information/-/cordova-plugin-network-information-2.0.2.tgz", "integrity": "sha512-NwO3qDBNL/vJxUxBTPNOA1HvkDf9eTeGH8JSZiwy1jq2W2mJKQEDBwqWkaEQS19Yd/MQTiw0cykxg5D7u4J6cQ==" }, + "cordova-plugin-qrscanner": { + "version": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#857efee3a7a49104faabd108ff1f00a57d3aca94", + "from": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist", + "requires": { + "qrcode-reader": "^1.0.4", + "webrtc-adapter": "^3.1.4" + } + }, "cordova-plugin-screen-orientation": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/cordova-plugin-screen-orientation/-/cordova-plugin-screen-orientation-3.0.2.tgz", @@ -2709,6 +3768,14 @@ "resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.4.tgz", "integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ==" }, + "cordova-plugin-wkuserscript": { + "version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#6413f4bb3c2565f353e690b5c1450b69ad9e860e", + "from": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git" + }, + "cordova-plugin-wkwebview-cookies": { + "version": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git#8c3a289e29b33edecff15f470c1630baf4ec3e88", + "from": "git+https://github.com/moodlemobile/cordova-plugin-wkwebview-cookies.git" + }, "cordova-plugin-zip": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cordova-plugin-zip/-/cordova-plugin-zip-3.1.0.tgz", @@ -2867,6 +3934,12 @@ } } }, + "data-uri-to-buffer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", + "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==", + "dev": true + }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", @@ -2907,6 +3980,12 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, "default-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", @@ -3003,6 +4082,25 @@ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" }, + "degenerator": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-1.0.4.tgz", + "integrity": "sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=", + "dev": true, + "requires": { + "ast-types": "0.x.x", + "escodegen": "1.x.x", + "esprima": "3.x.x" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", + "dev": true + } + } + }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3174,6 +4272,15 @@ "dotenv-defaults": "^1.0.2" } }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -3957,6 +5064,23 @@ "resolved": "https://registry.npmjs.org/es6-promise-plugin/-/es6-promise-plugin-4.2.2.tgz", "integrity": "sha512-uoA4aVplXI9oqUYJFBAVRwAqIN9/n9JgrTAUGX3qPbnSZVE5yY1+6/MsoN5f4xsaPO62WjPHOdtts6okMN6tNA==" }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + }, + "dependencies": { + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + } + } + }, "es6-set": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", @@ -4002,6 +5126,19 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, + "escodegen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", + "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, "escope": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", @@ -4535,6 +5672,18 @@ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==", + "dev": true + }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -4544,6 +5693,15 @@ "websocket-driver": ">=0.5.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -4552,6 +5710,11 @@ "escape-string-regexp": "^1.0.5" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -4971,6 +6134,12 @@ "mime-types": "^2.1.12" } }, + "formidable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", + "dev": true + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -5048,13 +6217,14 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", + "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" + "node-pre-gyp": "*" }, "dependencies": { "abbrev": { @@ -5128,11 +6298,7 @@ "debug": { "version": "4.1.1", "resolved": false, - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==" }, "deep-extend": { "version": "0.6.0", @@ -5159,6 +6325,18 @@ "optional": true, "requires": { "minipass": "^2.2.1" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } } }, "fs.realpath": { @@ -5285,6 +6463,18 @@ "optional": true, "requires": { "minipass": "^2.2.1" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } } }, "mkdirp": { @@ -5293,14 +6483,15 @@ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } } }, - "ms": { - "version": "2.1.1", - "resolved": false, - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "optional": true - }, "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", @@ -5316,6 +6507,25 @@ "debug": "^4.1.0", "iconv-lite": "^0.4.4", "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "optional": true, + "requires": { + "ms": "^2.1.1" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + } + } + } } }, "node-pre-gyp": { @@ -5552,6 +6762,18 @@ "mkdirp": "^0.5.0", "safe-buffer": "^5.1.2", "yallist": "^3.0.2" + }, + "dependencies": { + "minipass": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", + "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + } } }, "util-deprecate": { @@ -5593,6 +6815,42 @@ "rimraf": "2" } }, + "ftp": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ftp/-/ftp-0.3.10.tgz", + "integrity": "sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=", + "dev": true, + "requires": { + "readable-stream": "1.1.x", + "xregexp": "2.0.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -5640,6 +6898,20 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, + "get-uri": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-2.0.4.tgz", + "integrity": "sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==", + "dev": true, + "requires": { + "data-uri-to-buffer": "1", + "debug": "2", + "extend": "~3.0.2", + "file-uri-to-path": "1", + "ftp": "~0.3.10", + "readable-stream": "2" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -5806,9 +7078,9 @@ } }, "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -6671,8 +7943,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, "micromatch": { @@ -6696,13 +7969,6 @@ "to-regex": "^3.0.2" } }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true - }, "readdirp": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", @@ -6715,9 +7981,9 @@ } }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true } } @@ -7245,6 +8511,27 @@ "integrity": "sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w==", "dev": true }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "dev": true, + "requires": { + "agent-base": "4", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -7261,6 +8548,33 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "dev": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -7558,6 +8872,12 @@ } } }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", @@ -7676,6 +8996,12 @@ } } }, + "is-docker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "dev": true + }, "is-dotfile": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", @@ -8137,6 +9463,40 @@ "flush-write-stream": "^1.0.2" } }, + "leek": { + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/leek/-/leek-0.0.24.tgz", + "integrity": "sha1-5ADlfw5g2O8r1NBo3EKKVDRdvNo=", + "dev": true, + "requires": { + "debug": "^2.1.0", + "lodash.assign": "^3.2.0", + "rsvp": "^3.0.21" + }, + "dependencies": { + "lodash.assign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", + "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._createassigner": "^3.0.0", + "lodash.keys": "^3.0.0" + } + } + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, "lie": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", @@ -8218,6 +9578,51 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", + "dev": true + }, + "lodash._createassigner": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", + "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", + "dev": true, + "requires": { + "lodash._bindcallback": "^3.0.0", + "lodash._isiterateecall": "^3.0.0", + "lodash.restparam": "^3.0.0" + } + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -8235,6 +9640,35 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, "lodash.template": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", @@ -8263,6 +9697,137 @@ "chalk": "^2.0.1" } }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", @@ -8847,9 +10412,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "2.9.0", @@ -8898,18 +10463,11 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } + "minimist": "^1.2.5" } }, "moment": { @@ -8934,9 +10492,9 @@ "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", "dev": true, "optional": true }, @@ -8976,15 +10534,70 @@ } } }, + "native-run": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/native-run/-/native-run-1.0.0.tgz", + "integrity": "sha512-BKHQM9oXRY7mIlOHex1EAeTJeg1Gy6EJEKvt1bWyrb387dcFYCIVGdCqKmbQXg4OjHrJw2caDrHoN9y7U9y2+A==", + "dev": true, + "requires": { + "@ionic/utils-fs": "^3.0.0", + "debug": "^4.1.1", + "elementtree": "^0.1.7", + "ini": "^1.3.5", + "node-ioslib": "0.0.9", + "split2": "^3.1.0", + "through2": "^3.0.0", + "tslib": "^1.9.3", + "yauzl": "^2.10.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "dev": true, + "requires": { + "readable-stream": "2 || 3" + } + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + } + } + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, "neo-async": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz", - "integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "dev": true + }, + "netmask": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-1.0.6.tgz", + "integrity": "sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=", "dev": true }, "next-tick": { @@ -9208,6 +10821,41 @@ } } }, + "node-ioslib": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/node-ioslib/-/node-ioslib-0.0.9.tgz", + "integrity": "sha512-rHRbH1glQ+mmC8EuBLl89OO/WjicAUszW3OKRzzHdedVyWZ/yD3eg3GzHkaNgWtTwFl/0x29OyC4aQp7LdYlYA==", + "dev": true, + "requires": { + "bplist-parser": "^0.1.1", + "debug": "^4.1.1", + "plist": "^3.0.1", + "tslib": "^1.9.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tslib": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", + "dev": true + } + } + }, "node-libs-browser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", @@ -9619,6 +11267,27 @@ "mimic-fn": "^1.0.0" } }, + "open": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-7.0.4.tgz", + "integrity": "sha512-brSA+/yq+b08Hsr4c8fsEW2CRzk1BmfN3SAK/5VCHQ9bdoZJ4qa/+AfR0xHjlbbZUyPkUHs1b8x1RqdyZdkVqQ==", + "dev": true, + "requires": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + } + } + }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -9627,6 +11296,20 @@ "is-wsl": "^1.1.0" } }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, "ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -9706,6 +11389,52 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" }, + "pac-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz", + "integrity": "sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==", + "dev": true, + "requires": { + "agent-base": "^4.2.0", + "debug": "^4.1.1", + "get-uri": "^2.0.0", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "pac-resolver": "^3.0.0", + "raw-body": "^2.2.0", + "socks-proxy-agent": "^4.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "pac-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-3.0.0.tgz", + "integrity": "sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==", + "dev": true, + "requires": { + "co": "^4.6.0", + "degenerator": "^1.0.4", + "ip": "^1.1.5", + "netmask": "^1.0.6", + "thunkify": "^2.1.2" + } + }, "package-json": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", @@ -9895,6 +11624,12 @@ "sha.js": "^2.4.8" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -10036,6 +11771,12 @@ "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", "dev": true }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, "prepend-http": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", @@ -10098,6 +11839,60 @@ "ipaddr.js": "1.8.0" } }, + "proxy-agent": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-3.1.1.tgz", + "integrity": "sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==", + "dev": true, + "requires": { + "agent-base": "^4.2.0", + "debug": "4", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^3.0.0", + "lru-cache": "^5.1.1", + "pac-proxy-agent": "^3.0.1", + "proxy-from-env": "^1.0.0", + "socks-proxy-agent": "^4.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "proxy-middleware": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz", @@ -10165,6 +11960,11 @@ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, + "qrcode-reader": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/qrcode-reader/-/qrcode-reader-1.0.4.tgz", + "integrity": "sha512-rRjALGNh9zVqvweg1j5OKIQKNsw3bLC+7qwlnead5K/9cb1cEIAGkwikt/09U0K+2IDWGD9CC6SP7tHAjUeqvQ==" + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -10980,6 +12780,12 @@ } } }, + "rsvp": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", + "integrity": "sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==", + "dev": true + }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", @@ -11068,6 +12874,11 @@ } } }, + "sdp": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-1.5.4.tgz", + "integrity": "sha1-jgOPbdsUvXZa4fS1IW4SCUUR4NA=" + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -11234,6 +13045,56 @@ "integrity": "sha1-qnEMjvULjh0YetbP9G84xla6Dlc=", "dev": true }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + } + } + }, + "smart-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", + "integrity": "sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==", + "dev": true + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -11323,7 +13184,8 @@ }, "kind-of": { "version": "6.0.2", - "resolved": "" + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" } } }, @@ -11345,6 +13207,37 @@ } } }, + "socks": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz", + "integrity": "sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==", + "dev": true, + "requires": { + "ip": "1.1.5", + "smart-buffer": "^4.1.0" + } + }, + "socks-proxy-agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz", + "integrity": "sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==", + "dev": true, + "requires": { + "agent-base": "~4.2.1", + "socks": "~2.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + } + } + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -11424,12 +13317,40 @@ "extend-shallow": "^3.0.0" } }, + "split2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.1.1.tgz", + "integrity": "sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q==", + "dev": true, + "requires": { + "readable-stream": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "ssh-config": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/ssh-config/-/ssh-config-1.1.6.tgz", + "integrity": "sha512-ZPO9rECxzs5JIQ6G/2EfL1I9ho/BVZkx9HRKn8+0af7QgwAmumQ7XBFP1ggMyPMo+/tUbmv0HFdv4qifdO/9JA==", + "dev": true + }, "sshpk": { "version": "1.14.2", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", @@ -11513,6 +13434,16 @@ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ=" }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, "stream-exhaust": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", @@ -11624,6 +13555,124 @@ "debug": "^2.2.0" } }, + "superagent": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.2.2.tgz", + "integrity": "sha512-pMWBUnIllK4ZTw7p/UaobiQPwAO5w/1NRRTDpV0FTVNmECztsxKspj3ZWEordVEaqpZtmOQJJna4yTLyC/q7PQ==", + "dev": true, + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.1", + "methods": "^1.1.2", + "mime": "^2.4.4", + "qs": "^6.9.1", + "readable-stream": "^3.4.0", + "semver": "^6.3.0" + }, + "dependencies": { + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "superagent-proxy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/superagent-proxy/-/superagent-proxy-2.0.0.tgz", + "integrity": "sha512-TktJma5jPdiH1BNN+reF/RMW3b8aBTCV7KlLFV0uYcREgNf3pvo7Rdt564OcFHwkGb3mYEhHuWPBhSbOwiNaYw==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "proxy-agent": "3" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", @@ -11790,6 +13839,12 @@ "xtend": "~4.0.0" } }, + "thunkify": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/thunkify/-/thunkify-2.1.2.tgz", + "integrity": "sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=", + "dev": true + }, "time-stamp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", @@ -11941,6 +13996,12 @@ "punycode": "^1.4.1" } }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", @@ -12054,6 +14115,21 @@ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "optional": true }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + }, "type-is": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", @@ -12069,6 +14145,15 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "typescript": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", @@ -12284,15 +14369,21 @@ } } }, + "untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true + }, "unzip-response": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" }, "upath": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz", - "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, "update-notifier": { @@ -12553,12 +14644,12 @@ } }, "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", + "integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==", "dev": true, "requires": { - "chokidar": "^2.0.2", + "chokidar": "^2.1.8", "graceful-fs": "^4.1.2", "neo-async": "^2.5.0" }, @@ -12626,9 +14717,9 @@ } }, "chokidar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.1.tgz", - "integrity": "sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -12642,7 +14733,7 @@ "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", "readdirp": "^2.2.1", - "upath": "^1.1.0" + "upath": "^1.1.1" } }, "expand-brackets": { @@ -13448,9 +15539,9 @@ "dev": true }, "is-glob": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", - "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, "requires": { "is-extglob": "^2.1.1" @@ -13482,11 +15573,6 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, - "kind-of": { - "version": "6.0.2", - "resolved": "", - "dev": true - }, "micromatch": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", @@ -13784,6 +15870,14 @@ "source-map": "~0.6.1" } }, + "webrtc-adapter": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-3.4.3.tgz", + "integrity": "sha1-tjYGLu6abvFYrNDYUBtnhDS1bxY=", + "requires": { + "sdp": "^1.5.0" + } + }, "websocket-driver": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", @@ -13950,6 +16044,12 @@ } } }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", @@ -14031,6 +16131,12 @@ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" }, + "xregexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz", + "integrity": "sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM=", + "dev": true + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", @@ -14094,6 +16200,16 @@ } } }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "zone.js": { "version": "0.8.29", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.29.tgz", diff --git a/package.json b/package.json index bd1de5e56..8268eef27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moodlemobile", - "version": "3.8.2", + "version": "3.9.0", "description": "The official app for Moodle.", "author": { "name": "Moodle Pty Ltd.", @@ -24,6 +24,12 @@ } ], "scripts": { + "start": "npm run dev", + "dev": "ionic serve", + "dev:android": "ionic cordova run android --livereload", + "dev:ios": "ionic cordova run ios --livereload", + "prod:android": "ionic cordova run android --aot", + "prod:ios": "ionic cordova run ios --aot", "setup": "npm install && npx cordova prepare && npx gulp", "clean": "npx ionic-app-scripts clean", "build": "npx ionic-app-scripts build", @@ -51,6 +57,7 @@ "@angular/platform-browser-dynamic": "5.2.11", "@ionic-native/badge": "4.20.0", "@ionic-native/camera": "4.20.0", + "@ionic-native/chooser": "^4.20.0", "@ionic-native/clipboard": "4.20.0", "@ionic-native/core": "4.20.0", "@ionic-native/device": "4.20.0", @@ -59,12 +66,14 @@ "@ionic-native/file-transfer": "4.20.0", "@ionic-native/geolocation": "4.20.0", "@ionic-native/globalization": "4.20.0", + "@ionic-native/http": "^4.20.0", "@ionic-native/in-app-browser": "4.20.0", "@ionic-native/keyboard": "4.20.0", "@ionic-native/local-notifications": "4.20.0", "@ionic-native/media-capture": "4.20.0", "@ionic-native/network": "4.20.0", "@ionic-native/push": "4.20.0", + "@ionic-native/qr-scanner": "4.20.0", "@ionic-native/screen-orientation": "4.20.0", "@ionic-native/splash-screen": "4.20.0", "@ionic-native/sqlite": "4.20.0", @@ -81,24 +90,30 @@ "cordova-android-support-gradle-release": "3.0.1", "cordova-clipboard": "1.3.0", "cordova-ios": "5.1.1", + "cordova-plugin-advanced-http": "2.4.1", "cordova-plugin-badge": "0.8.8", "cordova-plugin-camera": "4.1.0", + "cordova-plugin-chooser": "1.3.1", "cordova-plugin-customurlscheme": "5.0.0", "cordova-plugin-device": "2.0.3", "cordova-plugin-file": "6.0.2", "cordova-plugin-file-opener2": "3.0.0", "cordova-plugin-file-transfer": "1.7.1", - "cordova-plugin-geolocation": "4.0.2", + "cordova-plugin-geolocation": "git+https://github.com/apache/cordova-plugin-geolocation.git#89cf51d222e8f225bdfb661965b3007d669c40ff", "cordova-plugin-globalization": "1.11.0", - "cordova-plugin-inappbrowser": "3.2.0", + "cordova-plugin-inappbrowser": "4.0.0", "cordova-plugin-ionic-keyboard": "2.1.3", + "cordova-plugin-ionic-webview": "4.1.3", "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", "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-splashscreen": "5.0.3", "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-sqlite-storage": "4.0.0", "cordova-support-google-services": "1.3.2", @@ -121,12 +136,14 @@ }, "devDependencies": { "@ionic/app-scripts": "3.2.3", + "@ionic/cli": "^6.9.3", "@types/cordova": "0.0.34", "@types/cordova-plugin-file-transfer": "0.0.3", "@types/cordova-plugin-globalization": "0.0.3", "@types/cordova-plugin-network-information": "0.0.3", "@types/node": "8.10.59", "@types/promise.prototype.finally": "2.0.4", + "acorn": "^5.7.4", "electron-builder-lib": "20.23.1", "electron-rebuild": "1.10.0", "gulp": "4.0.2", @@ -137,6 +154,8 @@ "gulp-rename": "2.0.0", "gulp-slash": "1.1.3", "lodash.template": "4.5.0", + "minimist": "^1.2.5", + "native-run": "^1.0.0", "node-loader": "0.6.0", "through": "2.3.8", "typescript": "2.6.2", @@ -181,11 +200,20 @@ "nl.kingsquare.cordova.background-audio": {}, "phonegap-plugin-push": { "ANDROID_SUPPORT_V13_VERSION": "27.+", - "FCM_VERSION": "17.0.+" + "FCM_VERSION": "17.5.+" }, "cordova-plugin-geolocation": { - "GEOLOCATION_USAGE_DESCRIPTION": "To locate you" - } + "GEOLOCATION_USAGE_DESCRIPTION": "We need your location so you can attach it as part of your submissions.", + "GPS_REQUIRED": "false" + }, + "cordova-plugin-ionic-webview": {}, + "cordova-plugin-advanced-http": { + "OKHTTP_VERSION": "3.10.0" + }, + "cordova-plugin-wkwebview-cookies": {}, + "cordova-plugin-qrscanner": {}, + "cordova-plugin-chooser": {}, + "cordova-plugin-wkuserscript": {} } }, "main": "desktop/electron.js", @@ -225,7 +253,7 @@ "category": "public.app-category.education", "icon": "resources/desktop/icon.icns", "target": "mas", - "bundleVersion": "3.8.2", + "bundleVersion": "3.9.0", "extendInfo": { "ElectronTeamID": "2NU57U5PAW" } @@ -244,5 +272,8 @@ "nsis": { "deleteAppDataOnUninstall": true } + }, + "engines": { + "node": "11.x" } -} +} \ No newline at end of file diff --git a/resources/android/icon-foreground.svg b/resources/android/icon-foreground.svg deleted file mode 100644 index add166f8b..000000000 --- a/resources/android/icon-foreground.svg +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - image/svg+xml - - MoodleApp_Icon_White_RGB - - - - - - - - MoodleApp_Icon_White_RGB - - - - - - - - diff --git a/resources/android/icon.png b/resources/android/icon.png deleted file mode 100644 index 59ea30d89..000000000 Binary files a/resources/android/icon.png and /dev/null differ diff --git a/resources/android/icon.png.md5 b/resources/android/icon.png.md5 deleted file mode 100644 index baafd389e..000000000 --- a/resources/android/icon.png.md5 +++ /dev/null @@ -1 +0,0 @@ -5e8ac0ef8768e0fad3284434d24064f8 \ No newline at end of file diff --git a/resources/android/icon/drawable-hdpi-icon.png b/resources/android/icon/drawable-hdpi-icon.png deleted file mode 100644 index e96fcfe45..000000000 Binary files a/resources/android/icon/drawable-hdpi-icon.png and /dev/null differ diff --git a/resources/android/icon/drawable-ldpi-icon.png b/resources/android/icon/drawable-ldpi-icon.png deleted file mode 100644 index 3becf6217..000000000 Binary files a/resources/android/icon/drawable-ldpi-icon.png and /dev/null differ diff --git a/resources/android/icon/drawable-mdpi-icon.png b/resources/android/icon/drawable-mdpi-icon.png deleted file mode 100644 index 16bb102e8..000000000 Binary files a/resources/android/icon/drawable-mdpi-icon.png and /dev/null differ diff --git a/resources/android/icon/drawable-xhdpi-icon.png b/resources/android/icon/drawable-xhdpi-icon.png deleted file mode 100644 index a43251c56..000000000 Binary files a/resources/android/icon/drawable-xhdpi-icon.png and /dev/null differ diff --git a/resources/android/icon/drawable-xxhdpi-icon.png b/resources/android/icon/drawable-xxhdpi-icon.png deleted file mode 100644 index 48ce4f3c9..000000000 Binary files a/resources/android/icon/drawable-xxhdpi-icon.png and /dev/null differ diff --git a/resources/android/icon/drawable-xxxhdpi-icon.png b/resources/android/icon/drawable-xxxhdpi-icon.png deleted file mode 100644 index 53e0e9d67..000000000 Binary files a/resources/android/icon/drawable-xxxhdpi-icon.png and /dev/null differ diff --git a/resources/android/icon/hdpi-foreground.png b/resources/android/icon/hdpi-foreground.png deleted file mode 100644 index 1616ddedf..000000000 Binary files a/resources/android/icon/hdpi-foreground.png and /dev/null differ diff --git a/resources/android/icon/ldpi-foreground.png b/resources/android/icon/ldpi-foreground.png deleted file mode 100644 index d24c14f7d..000000000 Binary files a/resources/android/icon/ldpi-foreground.png and /dev/null differ diff --git a/resources/android/icon/mdpi-foreground.png b/resources/android/icon/mdpi-foreground.png deleted file mode 100644 index 41ba86c39..000000000 Binary files a/resources/android/icon/mdpi-foreground.png and /dev/null differ diff --git a/resources/android/icon/xhdpi-foreground.png b/resources/android/icon/xhdpi-foreground.png deleted file mode 100644 index dcb852e00..000000000 Binary files a/resources/android/icon/xhdpi-foreground.png and /dev/null differ diff --git a/resources/android/icon/xxhdpi-foreground.png b/resources/android/icon/xxhdpi-foreground.png deleted file mode 100644 index fa39ca199..000000000 Binary files a/resources/android/icon/xxhdpi-foreground.png and /dev/null differ diff --git a/resources/android/icon/xxxhdpi-foreground.png b/resources/android/icon/xxxhdpi-foreground.png deleted file mode 100644 index 8ba201678..000000000 Binary files a/resources/android/icon/xxxhdpi-foreground.png and /dev/null differ diff --git a/resources/android/splash/drawable-land-hdpi-screen.png b/resources/android/splash/drawable-land-hdpi-screen.png deleted file mode 100644 index bf0f39e5a..000000000 Binary files a/resources/android/splash/drawable-land-hdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-land-ldpi-screen.png b/resources/android/splash/drawable-land-ldpi-screen.png deleted file mode 100644 index 38728e3eb..000000000 Binary files a/resources/android/splash/drawable-land-ldpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-land-mdpi-screen.png b/resources/android/splash/drawable-land-mdpi-screen.png deleted file mode 100644 index 6c94d5721..000000000 Binary files a/resources/android/splash/drawable-land-mdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-land-xhdpi-screen.png b/resources/android/splash/drawable-land-xhdpi-screen.png deleted file mode 100644 index 34d11f804..000000000 Binary files a/resources/android/splash/drawable-land-xhdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-land-xxhdpi-screen.png b/resources/android/splash/drawable-land-xxhdpi-screen.png deleted file mode 100644 index 8043bf1a7..000000000 Binary files a/resources/android/splash/drawable-land-xxhdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-land-xxxhdpi-screen.png b/resources/android/splash/drawable-land-xxxhdpi-screen.png deleted file mode 100644 index 3688f5732..000000000 Binary files a/resources/android/splash/drawable-land-xxxhdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-port-hdpi-screen.png b/resources/android/splash/drawable-port-hdpi-screen.png deleted file mode 100644 index 8c7353e9d..000000000 Binary files a/resources/android/splash/drawable-port-hdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-port-ldpi-screen.png b/resources/android/splash/drawable-port-ldpi-screen.png deleted file mode 100644 index b5605fe72..000000000 Binary files a/resources/android/splash/drawable-port-ldpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-port-mdpi-screen.png b/resources/android/splash/drawable-port-mdpi-screen.png deleted file mode 100644 index a3f133a16..000000000 Binary files a/resources/android/splash/drawable-port-mdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-port-xhdpi-screen.png b/resources/android/splash/drawable-port-xhdpi-screen.png deleted file mode 100644 index 777b8c86f..000000000 Binary files a/resources/android/splash/drawable-port-xhdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-port-xxhdpi-screen.png b/resources/android/splash/drawable-port-xxhdpi-screen.png deleted file mode 100644 index 58f7d76b1..000000000 Binary files a/resources/android/splash/drawable-port-xxhdpi-screen.png and /dev/null differ diff --git a/resources/android/splash/drawable-port-xxxhdpi-screen.png b/resources/android/splash/drawable-port-xxxhdpi-screen.png deleted file mode 100644 index 460d49a84..000000000 Binary files a/resources/android/splash/drawable-port-xxxhdpi-screen.png and /dev/null differ diff --git a/resources/icon.png b/resources/icon.png new file mode 100644 index 000000000..7abfc0a4f Binary files /dev/null and b/resources/icon.png differ diff --git a/resources/ios/icon.png b/resources/ios/icon.png deleted file mode 100644 index f41b1f803..000000000 Binary files a/resources/ios/icon.png and /dev/null differ diff --git a/resources/ios/icon.png.md5 b/resources/ios/icon.png.md5 deleted file mode 100644 index d951fb13a..000000000 --- a/resources/ios/icon.png.md5 +++ /dev/null @@ -1 +0,0 @@ -5225afcaf865b3e218501903bef688e0 \ No newline at end of file diff --git a/resources/ios/icon/icon-1024.png b/resources/ios/icon/icon-1024.png deleted file mode 100644 index 9df9c6fe2..000000000 Binary files a/resources/ios/icon/icon-1024.png and /dev/null differ diff --git a/resources/ios/icon/icon-108@2x.png b/resources/ios/icon/icon-108@2x.png deleted file mode 100644 index b1ee7709a..000000000 Binary files a/resources/ios/icon/icon-108@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-20.png b/resources/ios/icon/icon-20.png deleted file mode 100644 index eab5d3e35..000000000 Binary files a/resources/ios/icon/icon-20.png and /dev/null differ diff --git a/resources/ios/icon/icon-20@2x.png b/resources/ios/icon/icon-20@2x.png deleted file mode 100644 index 125cfc725..000000000 Binary files a/resources/ios/icon/icon-20@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-20@3x.png b/resources/ios/icon/icon-20@3x.png deleted file mode 100644 index bfa0ecdba..000000000 Binary files a/resources/ios/icon/icon-20@3x.png and /dev/null differ diff --git a/resources/ios/icon/icon-24@2x.png b/resources/ios/icon/icon-24@2x.png deleted file mode 100644 index e3b06bfc6..000000000 Binary files a/resources/ios/icon/icon-24@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-27.5@2x.png b/resources/ios/icon/icon-27.5@2x.png deleted file mode 100644 index 07b03695d..000000000 Binary files a/resources/ios/icon/icon-27.5@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-29.png b/resources/ios/icon/icon-29.png deleted file mode 100644 index fe94321bb..000000000 Binary files a/resources/ios/icon/icon-29.png and /dev/null differ diff --git a/resources/ios/icon/icon-29@2x.png b/resources/ios/icon/icon-29@2x.png deleted file mode 100644 index a0b6b0438..000000000 Binary files a/resources/ios/icon/icon-29@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-29@3x.png b/resources/ios/icon/icon-29@3x.png deleted file mode 100644 index 0756d9fed..000000000 Binary files a/resources/ios/icon/icon-29@3x.png and /dev/null differ diff --git a/resources/ios/icon/icon-40.png b/resources/ios/icon/icon-40.png deleted file mode 100644 index 125cfc725..000000000 Binary files a/resources/ios/icon/icon-40.png and /dev/null differ diff --git a/resources/ios/icon/icon-40@2x.png b/resources/ios/icon/icon-40@2x.png deleted file mode 100644 index 25fb10e3a..000000000 Binary files a/resources/ios/icon/icon-40@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-40@3x.png b/resources/ios/icon/icon-40@3x.png deleted file mode 100644 index 9ec2a7f12..000000000 Binary files a/resources/ios/icon/icon-40@3x.png and /dev/null differ diff --git a/resources/ios/icon/icon-44@2x.png b/resources/ios/icon/icon-44@2x.png deleted file mode 100644 index e1f2e56bf..000000000 Binary files a/resources/ios/icon/icon-44@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-50.png b/resources/ios/icon/icon-50.png deleted file mode 100644 index 462c1ddda..000000000 Binary files a/resources/ios/icon/icon-50.png and /dev/null differ diff --git a/resources/ios/icon/icon-50@2x.png b/resources/ios/icon/icon-50@2x.png deleted file mode 100644 index 9b85f2603..000000000 Binary files a/resources/ios/icon/icon-50@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-60.png b/resources/ios/icon/icon-60.png deleted file mode 100644 index bfa0ecdba..000000000 Binary files a/resources/ios/icon/icon-60.png and /dev/null differ diff --git a/resources/ios/icon/icon-60@2x.png b/resources/ios/icon/icon-60@2x.png deleted file mode 100644 index 9ec2a7f12..000000000 Binary files a/resources/ios/icon/icon-60@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-60@3x.png b/resources/ios/icon/icon-60@3x.png deleted file mode 100644 index 781bfc7eb..000000000 Binary files a/resources/ios/icon/icon-60@3x.png and /dev/null differ diff --git a/resources/ios/icon/icon-72.png b/resources/ios/icon/icon-72.png deleted file mode 100644 index 948cb58bf..000000000 Binary files a/resources/ios/icon/icon-72.png and /dev/null differ diff --git a/resources/ios/icon/icon-72@2x.png b/resources/ios/icon/icon-72@2x.png deleted file mode 100644 index 57192935c..000000000 Binary files a/resources/ios/icon/icon-72@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-76.png b/resources/ios/icon/icon-76.png deleted file mode 100644 index a646758f9..000000000 Binary files a/resources/ios/icon/icon-76.png and /dev/null differ diff --git a/resources/ios/icon/icon-76@2x.png b/resources/ios/icon/icon-76@2x.png deleted file mode 100644 index b52383bf9..000000000 Binary files a/resources/ios/icon/icon-76@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-83.5@2x.png b/resources/ios/icon/icon-83.5@2x.png deleted file mode 100644 index 9b2542500..000000000 Binary files a/resources/ios/icon/icon-83.5@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-86@2x.png b/resources/ios/icon/icon-86@2x.png deleted file mode 100644 index c791d4cab..000000000 Binary files a/resources/ios/icon/icon-86@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-98@2x.png b/resources/ios/icon/icon-98@2x.png deleted file mode 100644 index 01f600e0c..000000000 Binary files a/resources/ios/icon/icon-98@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-small.png b/resources/ios/icon/icon-small.png deleted file mode 100644 index fe94321bb..000000000 Binary files a/resources/ios/icon/icon-small.png and /dev/null differ diff --git a/resources/ios/icon/icon-small@2x.png b/resources/ios/icon/icon-small@2x.png deleted file mode 100644 index a0b6b0438..000000000 Binary files a/resources/ios/icon/icon-small@2x.png and /dev/null differ diff --git a/resources/ios/icon/icon-small@3x.png b/resources/ios/icon/icon-small@3x.png deleted file mode 100644 index 0756d9fed..000000000 Binary files a/resources/ios/icon/icon-small@3x.png and /dev/null differ diff --git a/resources/ios/icon/icon.png b/resources/ios/icon/icon.png deleted file mode 100644 index 23aec9534..000000000 Binary files a/resources/ios/icon/icon.png and /dev/null differ diff --git a/resources/ios/icon/icon@2x.png b/resources/ios/icon/icon@2x.png deleted file mode 100644 index e18bedc51..000000000 Binary files a/resources/ios/icon/icon@2x.png and /dev/null differ diff --git a/resources/ios/splash/Default-1792h~iphone.png b/resources/ios/splash/Default-1792h~iphone.png deleted file mode 100644 index e725ae101..000000000 Binary files a/resources/ios/splash/Default-1792h~iphone.png and /dev/null differ diff --git a/resources/ios/splash/Default-2436h.png b/resources/ios/splash/Default-2436h.png deleted file mode 100644 index 4e7fb644c..000000000 Binary files a/resources/ios/splash/Default-2436h.png and /dev/null differ diff --git a/resources/ios/splash/Default-2688h~iphone.png b/resources/ios/splash/Default-2688h~iphone.png deleted file mode 100644 index af1b72ec8..000000000 Binary files a/resources/ios/splash/Default-2688h~iphone.png and /dev/null differ diff --git a/resources/ios/splash/Default-568h@2x~iphone.png b/resources/ios/splash/Default-568h@2x~iphone.png deleted file mode 100644 index 6e5005418..000000000 Binary files a/resources/ios/splash/Default-568h@2x~iphone.png and /dev/null differ diff --git a/resources/ios/splash/Default-667h.png b/resources/ios/splash/Default-667h.png deleted file mode 100644 index 6c2e7b17e..000000000 Binary files a/resources/ios/splash/Default-667h.png and /dev/null differ diff --git a/resources/ios/splash/Default-736h.png b/resources/ios/splash/Default-736h.png deleted file mode 100644 index 51e8cffc6..000000000 Binary files a/resources/ios/splash/Default-736h.png and /dev/null differ diff --git a/resources/ios/splash/Default-Landscape-1792h~iphone.png b/resources/ios/splash/Default-Landscape-1792h~iphone.png deleted file mode 100644 index ed0dcc13c..000000000 Binary files a/resources/ios/splash/Default-Landscape-1792h~iphone.png and /dev/null differ diff --git a/resources/ios/splash/Default-Landscape-2436h.png b/resources/ios/splash/Default-Landscape-2436h.png deleted file mode 100644 index 8933029ae..000000000 Binary files a/resources/ios/splash/Default-Landscape-2436h.png and /dev/null differ diff --git a/resources/ios/splash/Default-Landscape-2688h~iphone.png b/resources/ios/splash/Default-Landscape-2688h~iphone.png deleted file mode 100644 index 02a152e5e..000000000 Binary files a/resources/ios/splash/Default-Landscape-2688h~iphone.png and /dev/null differ diff --git a/resources/ios/splash/Default-Landscape-736h.png b/resources/ios/splash/Default-Landscape-736h.png deleted file mode 100644 index 8123091af..000000000 Binary files a/resources/ios/splash/Default-Landscape-736h.png and /dev/null differ diff --git a/resources/ios/splash/Default-Landscape@2x~ipad.png b/resources/ios/splash/Default-Landscape@2x~ipad.png deleted file mode 100644 index 63398a5cd..000000000 Binary files a/resources/ios/splash/Default-Landscape@2x~ipad.png and /dev/null differ diff --git a/resources/ios/splash/Default-Landscape@~ipadpro.png b/resources/ios/splash/Default-Landscape@~ipadpro.png deleted file mode 100644 index add5c8016..000000000 Binary files a/resources/ios/splash/Default-Landscape@~ipadpro.png and /dev/null differ diff --git a/resources/ios/splash/Default-Landscape~ipad.png b/resources/ios/splash/Default-Landscape~ipad.png deleted file mode 100644 index 6e5fab597..000000000 Binary files a/resources/ios/splash/Default-Landscape~ipad.png and /dev/null differ diff --git a/resources/ios/splash/Default-Portrait@2x~ipad.png b/resources/ios/splash/Default-Portrait@2x~ipad.png deleted file mode 100644 index 1ebad1ba7..000000000 Binary files a/resources/ios/splash/Default-Portrait@2x~ipad.png and /dev/null differ diff --git a/resources/ios/splash/Default-Portrait@~ipadpro.png b/resources/ios/splash/Default-Portrait@~ipadpro.png deleted file mode 100644 index c158dafff..000000000 Binary files a/resources/ios/splash/Default-Portrait@~ipadpro.png and /dev/null differ diff --git a/resources/ios/splash/Default-Portrait~ipad.png b/resources/ios/splash/Default-Portrait~ipad.png deleted file mode 100644 index f7a7b564b..000000000 Binary files a/resources/ios/splash/Default-Portrait~ipad.png and /dev/null differ diff --git a/resources/ios/splash/Default@2x~iphone.png b/resources/ios/splash/Default@2x~iphone.png deleted file mode 100644 index 7149bb0d4..000000000 Binary files a/resources/ios/splash/Default@2x~iphone.png and /dev/null differ diff --git a/resources/ios/splash/Default@2x~universal~anyany.png b/resources/ios/splash/Default@2x~universal~anyany.png deleted file mode 100644 index 1bb650ee0..000000000 Binary files a/resources/ios/splash/Default@2x~universal~anyany.png and /dev/null differ diff --git a/resources/ios/splash/Default~iphone.png b/resources/ios/splash/Default~iphone.png deleted file mode 100644 index a3f133a16..000000000 Binary files a/resources/ios/splash/Default~iphone.png and /dev/null differ diff --git a/resources/splash.png.md5 b/resources/splash.png.md5 deleted file mode 100644 index 808167787..000000000 --- a/resources/splash.png.md5 +++ /dev/null @@ -1 +0,0 @@ -4d2128e5cc9659b321956c1178057980 \ No newline at end of file diff --git a/scripts/aot.sh b/scripts/aot.sh new file mode 100755 index 000000000..4ebb56b0d --- /dev/null +++ b/scripts/aot.sh @@ -0,0 +1,32 @@ +#!/bin/bash +source "scripts/functions.sh" + +if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] ; then + print_title "Run scripts" + git clone --depth 1 https://$GIT_TOKEN@github.com/$GIT_ORG_PRIVATE/apps-scripts.git ../scripts + cp ../scripts/*.sh scripts/ + + if [ $TRAVIS_BUILD_STAGE_NAME == 'prepare' ] && [ -f scripts/prepare.sh ] ; then + print_title 'Prepare Build' + ./scripts/prepare.sh + + if [ $? -ne 0 ]; then + exit 1 + fi + elif [ $TRAVIS_BUILD_STAGE_NAME != 'prepare' ] && [ -f scripts/platform.sh ]; then + print_title 'Platform Build' + ./scripts/platform.sh + + if [ $? -ne 0 ]; then + exit 1 + fi + fi +else + print_title "AOT Compilation" + # Dynamic template loading without errors. + sed -ie $'s~throw new Error("No ResourceLoader.*~url = "templates/" + url;\\\nvar resolve;\\\nvar reject;\\\nvar promise = new Promise(function (res, rej) {\\\nresolve = res;\\\nreject = rej;\\\n});\\\nvar xhr = new XMLHttpRequest();\\\nxhr.open("GET", url, true);\\\nxhr.responseType = "text";\\\nxhr.onload = function () {\\\nvar response = xhr.response || xhr.responseText;\\\nvar status = xhr.status === 1223 ? 204 : xhr.status;\\\nif (status === 0) {\\\nstatus = response ? 200 : 0;\\\n}\\\nif (200 <= status \&\& status <= 300) {\\\nresolve(response);\\\n}\\\nelse {\\\nreject("Failed to load " + url);\\\n}\\\n};\\\nxhr.onerror = function () { reject("Failed to load " + url); };\\\nxhr.send();\\\nreturn promise;\\\n~g' node_modules/@angular/platform-browser-dynamic/esm5/platform-browser-dynamic.js + # Do not run JS optimizations to avoid problems with site plugins. + sed -ie "s/context\.isProd || hasArg('--optimizeJs')/false/g" node_modules/@ionic/app-scripts/dist/util/config.js + npm run ionic:build -- --prod +fi + diff --git a/scripts/ci.sh b/scripts/ci.sh deleted file mode 100755 index 0fe5022a1..000000000 --- a/scripts/ci.sh +++ /dev/null @@ -1,81 +0,0 @@ -#!/bin/bash -source "scripts/functions.sh" - -if [ "$TRAVIS_EVENT_TYPE" == 'cron' ] ; then - # Tests scripts. - print_error 'CRON NOT IMPLEMENTED YET' -else - if [ -z $GIT_ORG_PRIVATE ] || [ -z $GIT_TOKEN ]; then - print_error "Env vars not correctly defined" - exit 1 - fi - - # List first level of installed libraries so we can check the installed versions. - print_title "NPM packages list" - npm list --depth=0 - - if [ "$TRAVIS_REPO_SLUG" == 'moodlehq/moodleapp' ]; then - if [ "$TRAVIS_BRANCH" == 'master' ]; then - print_title "Update langpacks" - cd scripts - ./update_lang.sh - cd .. - - print_title "Update generated lang files" - git remote set-url origin https://$GIT_TOKEN@github.com/$TRAVIS_REPO_SLUG.git - git fetch -q origin - git add -A src/assets/lang - git add */en.json - git add src/config.json - git commit -m 'Update lang files [ci skip]' - - print_title "Update Licenses" - npm install -g license-checker - - jq --version - license-checker --json --production --relativeLicensePath > licenses.json - jq 'del(.[].path)' licenses.json > licenses_old.json - mv licenses_old.json licenses.json - licenses=`jq -r 'keys[]' licenses.json` - echo "{" > licensesurl.json - first=1 - for license in $licenses; do - obj=`jq --arg lic $license '.[$lic]' licenses.json` - licensePath=`echo $obj | jq -r '.licenseFile'` - file="" - if [[ ! -z "$licensePath" ]] || [[ "$licensePath" != "null" ]]; then - file=$(basename $licensePath) - if [ $first -eq 1 ] ; then - first=0 - echo "\"$license\" : { \"licenseFile\" : \"$file\"}" >> licensesurl.json - else - echo ",\"$license\" : { \"licenseFile\" : \"$file\"}" >> licensesurl.json - fi - fi - done - echo "}" >> licensesurl.json - - jq -s '.[0] * .[1]' licenses.json licensesurl.json > licenses_old.json - mv licenses_old.json licenses.json - rm licensesurl.json - - git add licenses.json - git commit -m 'Update licenses [ci skip]' - - git push origin HEAD:$TRAVIS_BRANCH - fi - - if [ "$TRAVIS_BRANCH" == 'integration' ] || [ "$TRAVIS_BRANCH" == 'master' ] || [ "$TRAVIS_BRANCH" == 'desktop' ] ; then - print_title "Mirror repository" - git remote add mirror https://$GIT_TOKEN@github.com/$GIT_ORG_PRIVATE/moodleapp.git - git fetch -q --unshallow mirror - git push -f mirror HEAD:$TRAVIS_BRANCH - git push mirror --tags - fi - elif [ "$TRAVIS_REPO_SLUG" == "$GIT_ORG_PRIVATE/moodleapp" ]; then - print_title "Run scripts" - git clone --depth 1 https://$GIT_TOKEN@github.com/$GIT_ORG_PRIVATE/apps-scripts.git ../scripts - cp ../scripts/build.sh scripts/ - ./scripts/build.sh - fi -fi diff --git a/scripts/functions.sh b/scripts/functions.sh index 55af8536b..c875be20f 100644 --- a/scripts/functions.sh +++ b/scripts/functions.sh @@ -1,6 +1,16 @@ #!/bin/bash LANGPACKSFOLDER='../../moodle-langpacks' +stepnumber=$1 + +function check_success_exit { + if [ $? -ne 0 ]; then + print_error "$1" + exit 1 + elif [ "$#" -gt 1 ]; then + print_ok "$2" + fi +} function check_success { if [ $? -ne 0 ]; then @@ -38,4 +48,21 @@ function print_title { echo tput setaf 5; echo "$stepnumber $1"; tput sgr0 tput setaf 5; echo '=================='; tput sgr0 -} \ No newline at end of file +} + +function telegram_notify { + if [ ! -z $TELEGRAM_APIKEY ] && [ ! -z $TELEGRAM_CHATID ] ; then + MESSAGE="Travis error: $1%0ABranch: $TRAVIS_BRANCH%0ARepo: $TRAVIS_REPO_SLUG" + URL="https://api.telegram.org/bot$TELEGRAM_APIKEY/sendMessage" + + curl -s -X POST $URL -d chat_id=$TELEGRAM_CHATID -d text="$MESSAGE" + fi +} + +function notify_on_error_exit { + if [ $? -ne 0 ]; then + print_error "$1" + telegram_notify "$1" + exit 1 + fi +} diff --git a/scripts/get_ws_changes.php b/scripts/get_ws_changes.php new file mode 100644 index 000000000..fff119f1b --- /dev/null +++ b/scripts/get_ws_changes.php @@ -0,0 +1,99 @@ +. + +/** + * Script for detecting changes in a WS params or return data, version by version. + * + * The first parameter (required) is the path to the Moodle installation to use. + * The second parameter (required) is the name to the WS to convert. + * The third parameter (optional) is a number: 1 to convert the params structure, + * 0 to convert the returns structure. Defaults to 0. + */ + +if (!isset($argv[1])) { + echo "ERROR: Please pass the path to the folder containing the Moodle installations as the first parameter.\n"; + die(); +} + + +if (!isset($argv[2])) { + echo "ERROR: Please pass the WS name as the second parameter.\n"; + die(); +} + +define('CLI_SCRIPT', true); +require_once('ws_to_ts_functions.php'); + +$versions = array('master', '38', '37', '36', '35', '34', '33', '32', '31'); + +$moodlespath = $argv[1]; +$wsname = $argv[2]; +$useparams = !!(isset($argv[3]) && $argv[3]); +$pathseparator = '/'; + +// Get the path to the script. +$index = strrpos(__FILE__, $pathseparator); +if ($index === false) { + $pathseparator = '\\'; + $index = strrpos(__FILE__, $pathseparator); +} +$scriptfolder = substr(__FILE__, 0, $index); +$scriptpath = concatenate_paths($scriptfolder, 'get_ws_structure.php', $pathseparator); + +$previousstructure = null; +$previousversion = null; +$libsloaded = false; + +foreach ($versions as $version) { + $moodlepath = concatenate_paths($moodlespath, 'stable_' . $version, $pathseparator); + + if (!$libsloaded) { + $libsloaded = true; + + require($moodlepath . '/config.php'); + require($CFG->dirroot . '/webservice/lib.php'); + } + + // Get the structure in this Moodle version. + $structure = shell_exec("php $scriptpath $moodlepath $wsname " . ($useparams ? 'true' : '')); + + if (strpos($structure, 'ERROR:') === 0) { + echo "WS not found in version $version. Stop.\n"; + break; + } + + $structure = unserialize($structure); + + if ($previousstructure != null) { + echo "*** Check changes from version $version to $previousversion ***\n"; + + $messages = detect_ws_changes($previousstructure, $structure); + + if (count($messages) > 0) { + $haschanged = true; + + foreach($messages as $message) { + echo "$message\n"; + } + } else { + echo "No changes found.\n"; + } + echo "\n"; + } + + $previousstructure = $structure; + $previousversion = $version; +} diff --git a/scripts/get_ws_structure.php b/scripts/get_ws_structure.php new file mode 100644 index 000000000..bfa975ee9 --- /dev/null +++ b/scripts/get_ws_structure.php @@ -0,0 +1,55 @@ +. + +/** + * Script for getting the PHP structure of a WS returns or params. + * + * The first parameter (required) is the path to the Moodle installation to use. + * The second parameter (required) is the name to the WS to convert. + * The third parameter (optional) is a number: 1 to convert the params structure, + * 0 to convert the returns structure. Defaults to 0. + */ + +if (!isset($argv[1])) { + echo "ERROR: Please pass the Moodle path as the first parameter.\n"; + die(); +} + + +if (!isset($argv[2])) { + echo "ERROR: Please pass the WS name as the second parameter.\n"; + die(); +} + +$moodlepath = $argv[1]; +$wsname = $argv[2]; +$useparams = !!(isset($argv[3]) && $argv[3]); + +define('CLI_SCRIPT', true); + +require($moodlepath . '/config.php'); +require($CFG->dirroot . '/webservice/lib.php'); +require_once('ws_to_ts_functions.php'); + +$structure = get_ws_structure($wsname, $useparams); + +if ($structure === false) { + echo "ERROR: The WS wasn't found in this Moodle installation.\n"; + die(); +} + +remove_default_closures($structure); +echo serialize($structure); diff --git a/scripts/get_ws_ts.php b/scripts/get_ws_ts.php new file mode 100644 index 000000000..900a153b8 --- /dev/null +++ b/scripts/get_ws_ts.php @@ -0,0 +1,62 @@ +. + +/** + * Script for converting a PHP WS structure to a TS type. + * + * The first parameter (required) is the path to the Moodle installation to use. + * The second parameter (required) is the name to the WS to convert. + * The third parameter (optional) is the name to put to the TS type. Defaults to "TypeName". + * The fourth parameter (optional) is a number: 1 to convert the params structure, + * 0 to convert the returns structure. Defaults to 0. + */ + +if (!isset($argv[1])) { + echo "ERROR: Please pass the Moodle path as the first parameter.\n"; + die(); +} + + +if (!isset($argv[2])) { + echo "ERROR: Please pass the WS name as the second parameter.\n"; + die(); +} + +$moodlepath = $argv[1]; +$wsname = $argv[2]; +$typename = isset($argv[3]) ? $argv[3] : 'TypeName'; +$useparams = !!(isset($argv[4]) && $argv[4]); + +define('CLI_SCRIPT', true); + +require($moodlepath . '/config.php'); +require($CFG->dirroot . '/webservice/lib.php'); +require_once('ws_to_ts_functions.php'); + +$structure = get_ws_structure($wsname, $useparams); + +if ($structure === false) { + echo "ERROR: The WS wasn't found in this Moodle installation.\n"; + die(); +} + +if ($useparams) { + $description = "Params of WS $wsname."; +} else { + $description = "Result of WS $wsname."; +} + +echo get_ts_doc(null, $description, '') . "export type $typename = " . convert_to_ts(null, $structure, $useparams) . ";\n"; diff --git a/scripts/langindex.json b/scripts/langindex.json index b8e6572af..2a5ecb26b 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -27,6 +27,7 @@ "addon.badges.version": "badges", "addon.badges.warnexpired": "badges", "addon.block_activitymodules.pluginname": "block_activity_modules", + "addon.block_activityresults.pluginname": "block_activity_results", "addon.block_badges.pluginname": "block_badges", "addon.block_blogmenu.pluginname": "block_blog_menu", "addon.block_blogrecent.pluginname": "block_blog_recent", @@ -48,6 +49,7 @@ "addon.block_myoverview.nocourses": "block_myoverview", "addon.block_myoverview.past": "block_myoverview", "addon.block_myoverview.pluginname": "block_myoverview", + "addon.block_myoverview.shortname": "block_myoverview", "addon.block_myoverview.title": "block_myoverview", "addon.block_newsitems.pluginname": "block_news_items", "addon.block_onlineusers.pluginname": "block_online_users", @@ -657,6 +659,40 @@ "addon.mod_glossary.noentriesfound": "local_moodlemobileapp", "addon.mod_glossary.searchquery": "local_moodlemobileapp", "addon.mod_glossary.tagarea_glossary_entries": "glossary", + "addon.mod_h5pactivity.all_attempts": "h5pactivity", + "addon.mod_h5pactivity.answer_checked": "h5pactivity", + "addon.mod_h5pactivity.answer_correct": "h5pactivity", + "addon.mod_h5pactivity.answer_fail": "h5pactivity", + "addon.mod_h5pactivity.answer_incorrect": "h5pactivity", + "addon.mod_h5pactivity.answer_pass": "h5pactivity", + "addon.mod_h5pactivity.attempt": "h5pactivity", + "addon.mod_h5pactivity.attempt_completion_no": "h5pactivity", + "addon.mod_h5pactivity.attempt_completion_yes": "h5pactivity", + "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_none": "h5pactivity", + "addon.mod_h5pactivity.completion": "h5pactivity", + "addon.mod_h5pactivity.downloadh5pfile": "local_moodlemobileapp", + "addon.mod_h5pactivity.duration": "h5pactivity", + "addon.mod_h5pactivity.errorgetactivity": "local_moodlemobileapp", + "addon.mod_h5pactivity.filestatenotdownloaded": "local_moodlemobileapp", + "addon.mod_h5pactivity.filestateoutdated": "local_moodlemobileapp", + "addon.mod_h5pactivity.maxscore": "h5pactivity", + "addon.mod_h5pactivity.modulenameplural": "h5pactivity", + "addon.mod_h5pactivity.myattempts": "h5pactivity", + "addon.mod_h5pactivity.no_compatible_track": "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.score": "h5pactivity", + "addon.mod_h5pactivity.score_out_of": "h5pactivity", + "addon.mod_h5pactivity.startdate": "h5pactivity", + "addon.mod_h5pactivity.totalscore": "h5pactivity", + "addon.mod_h5pactivity.viewattempt": "local_moodlemobileapp", "addon.mod_imscp.deploymenterror": "imscp", "addon.mod_imscp.modulenameplural": "imscp", "addon.mod_imscp.showmoduledescription": "local_moodlemobileapp", @@ -870,12 +906,10 @@ "addon.mod_scorm.highestattempt": "scorm", "addon.mod_scorm.incomplete": "scorm", "addon.mod_scorm.lastattempt": "scorm", - "addon.mod_scorm.mode": "scorm", "addon.mod_scorm.modulenameplural": "scorm", "addon.mod_scorm.newattempt": "scorm", "addon.mod_scorm.noattemptsallowed": "scorm", "addon.mod_scorm.noattemptsmade": "scorm", - "addon.mod_scorm.normal": "scorm", "addon.mod_scorm.notattempted": "scorm", "addon.mod_scorm.offlineattemptnote": "local_moodlemobileapp", "addon.mod_scorm.offlineattemptovermax": "local_moodlemobileapp", @@ -1012,6 +1046,7 @@ "addon.notifications.playsound": "local_moodlemobileapp", "addon.notifications.therearentnotificationsyet": "local_moodlemobileapp", "addon.storagemanager.deletecourse": "local_moodlemobileapp", + "addon.storagemanager.deletecourses": "local_moodlemobileapp", "addon.storagemanager.deletedatafrom": "local_moodlemobileapp", "addon.storagemanager.info": "local_moodlemobileapp", "addon.storagemanager.managestorage": "local_moodlemobileapp", @@ -1332,6 +1367,8 @@ "core.browser": "local_moodlemobileapp", "core.cancel": "moodle", "core.cannotconnect": "local_moodlemobileapp", + "core.cannotconnecttrouble": "local_moodlemobileapp", + "core.cannotconnectverify": "local_moodlemobileapp", "core.cannotdownloadfiles": "local_moodlemobileapp", "core.captureaudio": "local_moodlemobileapp", "core.capturedimage": "local_moodlemobileapp", @@ -1370,6 +1407,7 @@ "core.confirmdeletefile": "repository", "core.confirmgotabroot": "local_moodlemobileapp", "core.confirmgotabrootdefault": "local_moodlemobileapp", + "core.confirmleaveunknownchanges": "local_moodlemobileapp", "core.confirmloss": "local_moodlemobileapp", "core.confirmopeninbrowser": "local_moodlemobileapp", "core.considereddigitalminor": "moodle", @@ -1383,6 +1421,7 @@ "core.contentlinks.errorredirectothersite": "local_moodlemobileapp", "core.continue": "moodle", "core.copiedtoclipboard": "local_moodlemobileapp", + "core.copytoclipboard": "local_moodlemobileapp", "core.course": "moodle", "core.course.activitydisabled": "local_moodlemobileapp", "core.course.activitynotyetviewableremoteaddon": "local_moodlemobileapp", @@ -1390,6 +1429,7 @@ "core.course.allsections": "local_moodlemobileapp", "core.course.askadmintosupport": "local_moodlemobileapp", "core.course.availablespace": "local_moodlemobileapp", + "core.course.cannotdeletewhiledownloading": "local_moodlemobileapp", "core.course.confirmdeletemodulefiles": "local_moodlemobileapp", "core.course.confirmdownload": "local_moodlemobileapp", "core.course.confirmdownloadunknownsize": "local_moodlemobileapp", @@ -1611,6 +1651,7 @@ "core.h5p.editor": "h5p", "core.h5p.embed": "h5p", "core.h5p.embedtitle": "h5p", + "core.h5p.errorgetemail": "local_moodlemobileapp", "core.h5p.fullscreen": "h5p", "core.h5p.gpl": "h5p", "core.h5p.h5ptitle": "h5p", @@ -1707,7 +1748,21 @@ "core.login.emailnotmatch": "local_moodlemobileapp", "core.login.erroraccesscontrolalloworigin": "local_moodlemobileapp", "core.login.errordeletesite": "local_moodlemobileapp", + "core.login.errorexampleurl": "local_moodlemobileapp", "core.login.errorupdatesite": "local_moodlemobileapp", + "core.login.faqcannotconnectanswer": "local_moodlemobileapp", + "core.login.faqcannotconnectquestion": "local_moodlemobileapp", + "core.login.faqcannotfindmysiteanswer": "local_moodlemobileapp", + "core.login.faqcannotfindmysitequestion": "local_moodlemobileapp", + "core.login.faqsetupsiteanswer": "local_moodlemobileapp", + "core.login.faqsetupsitelinktitle": "local_moodlemobileapp", + "core.login.faqsetupsitequestion": "local_moodlemobileapp", + "core.login.faqtestappanswer": "local_moodlemobileapp", + "core.login.faqtestappquestion": "local_moodlemobileapp", + "core.login.faqwhatisurlanswer": "local_moodlemobileapp", + "core.login.faqwhatisurlquestion": "local_moodlemobileapp", + "core.login.faqwhereisqrcode": "local_moodlemobileapp", + "core.login.faqwhereisqrcodeanswer": "local_moodlemobileapp", "core.login.findyoursite": "local_moodlemobileapp", "core.login.firsttime": "moodle", "core.login.forcepasswordchangenotice": "moodle", @@ -1736,8 +1791,18 @@ "core.login.mobileservicesnotenabled": "local_moodlemobileapp", "core.login.mustconfirm": "moodle", "core.login.newaccount": "moodle", - "core.login.newsitedescription": "local_moodlemobileapp", "core.login.notloggedin": "local_moodlemobileapp", + "core.login.onboardingcreatemanagecourses": "local_moodlemobileapp", + "core.login.onboardingenrolmanagestudents": "local_moodlemobileapp", + "core.login.onboardinggetstarted": "local_moodlemobileapp", + "core.login.onboardingialreadyhaveasite": "local_moodlemobileapp", + "core.login.onboardingimalearner": "local_moodlemobileapp", + "core.login.onboardingimaneducator": "local_moodlemobileapp", + "core.login.onboardingineedasite": "local_moodlemobileapp", + "core.login.onboardingprovidefeedback": "local_moodlemobileapp", + "core.login.onboardingtoconnect": "local_moodlemobileapp", + "core.login.onboardingwelcome": "local_moodlemobileapp", + "core.login.or": "local_moodlemobileapp", "core.login.password": "moodle", "core.login.passwordforgotten": "moodle", "core.login.passwordforgotteninstructions2": "moodle", @@ -1747,8 +1812,6 @@ "core.login.policyagreement": "moodle", "core.login.policyagreementclick": "moodle", "core.login.potentialidps": "auth", - "core.login.problemconnectingerror": "local_moodlemobileapp", - "core.login.problemconnectingerrorcontinue": "local_moodlemobileapp", "core.login.profileinvaliddata": "admin", "core.login.recaptchachallengeimage": "local_moodlemobileapp", "core.login.recaptchaexpired": "local_moodlemobileapp", @@ -1777,6 +1840,8 @@ "core.login.usernotaddederror": "error", "core.login.visitchangepassword": "local_moodlemobileapp", "core.login.webservicesnotenabled": "local_moodlemobileapp", + "core.login.youcanstillconnectwithcredentials": "local_moodlemobileapp", + "core.login.yourenteredsite": "local_moodlemobileapp", "core.lostconnection": "local_moodlemobileapp", "core.mainmenu.changesite": "local_moodlemobileapp", "core.mainmenu.help": "moodle", @@ -1799,6 +1864,7 @@ "core.mod_folder": "folder/pluginname", "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", @@ -1816,6 +1882,7 @@ "core.more": "moodle", "core.mygroups": "group", "core.name": "moodle", + "core.needhelp": "local_moodlemobileapp", "core.networkerroriframemsg": "local_moodlemobileapp", "core.networkerrormsg": "local_moodlemobileapp", "core.never": "moodle", @@ -1843,6 +1910,7 @@ "core.online": "message", "core.openfullimage": "local_moodlemobileapp", "core.openinbrowser": "local_moodlemobileapp", + "core.openmodinbrowser": "local_moodlemobileapp", "core.othergroups": "group", "core.pagea": "moodle", "core.parentlanguage": "langconfig", @@ -1853,6 +1921,7 @@ "core.previous": "moodle", "core.proceed": "moodle", "core.pulltorefresh": "local_moodlemobileapp", + "core.qrscanner": "local_moodlemobileapp", "core.question.answer": "question", "core.question.answersaved": "question", "core.question.cannotdeterminestatus": "local_moodlemobileapp", @@ -1895,6 +1964,7 @@ "core.retry": "local_moodlemobileapp", "core.save": "moodle", "core.savechanges": "assign", + "core.scanqr": "local_moodlemobileapp", "core.search": "moodle", "core.searching": "local_moodlemobileapp", "core.searchresults": "moodle", @@ -1998,10 +2068,12 @@ "core.sizekb": "moodle", "core.sizemb": "moodle", "core.sizetb": "local_moodlemobileapp", + "core.skip": "tool_usertours", "core.sorry": "local_moodlemobileapp", "core.sort": "moodle", "core.sortby": "moodle", "core.start": "grouptool", + "core.storingfiles": "local_moodlemobileapp", "core.strftimedate": "langconfig", "core.strftimedatefullshort": "langconfig", "core.strftimedateshort": "langconfig", @@ -2090,6 +2162,7 @@ "core.warningofflinedatadeleted": "local_moodlemobileapp", "core.whatisyourage": "moodle", "core.wheredoyoulive": "moodle", + "core.whoissiteadmin": "local_moodlemobileapp", "core.whoops": "local_moodlemobileapp", "core.whyisthishappening": "local_moodlemobileapp", "core.whyisthisrequired": "moodle", diff --git a/scripts/mirror.sh b/scripts/mirror.sh new file mode 100755 index 000000000..32e4efa4a --- /dev/null +++ b/scripts/mirror.sh @@ -0,0 +1,80 @@ +#!/bin/bash +source "scripts/functions.sh" + +npm run build --bailOnLintError true --typeCheckOnLint true +if [ $? -ne 0 ]; then + exit 1 +fi + +if [ -z $GIT_ORG_PRIVATE ] || [ -z $GIT_TOKEN ]; then + print_error "Env vars not correctly defined" + exit 1 +fi + +# List first level of installed libraries so we can check the installed versions. +print_title "NPM packages list" +npm list --depth=0 + +if [ "$TRAVIS_BRANCH" == 'master' ]; then + print_title "Update langpacks" + cd scripts + ./update_lang.sh + cd .. + + print_title "Update generated lang files" + git remote set-url origin https://$GIT_TOKEN@github.com/$TRAVIS_REPO_SLUG.git + git fetch -q origin + git add -A src/assets/lang + git add */en.json + git add src/config.json + git commit -m 'Update lang files [ci skip]' + + print_title "Update Licenses" + npm install -g license-checker + + jq --version + license-checker --json --production --relativeLicensePath > licenses.json + jq 'del(.[].path)' licenses.json > licenses_old.json + mv licenses_old.json licenses.json + licenses=`jq -r 'keys[]' licenses.json` + echo "{" > licensesurl.json + first=1 + for license in $licenses; do + obj=`jq --arg lic $license '.[$lic]' licenses.json` + licensePath=`echo $obj | jq -r '.licenseFile'` + file="" + if [[ ! -z "$licensePath" ]] || [[ "$licensePath" != "null" ]]; then + file=$(basename $licensePath) + if [ $first -eq 1 ] ; then + first=0 + echo "\"$license\" : { \"licenseFile\" : \"$file\"}" >> licensesurl.json + else + echo ",\"$license\" : { \"licenseFile\" : \"$file\"}" >> licensesurl.json + fi + fi + done + echo "}" >> licensesurl.json + + jq -s '.[0] * .[1]' licenses.json licensesurl.json > licenses_old.json + mv licenses_old.json licenses.json + rm licensesurl.json + + git add licenses.json + git commit -m 'Update licenses [ci skip]' + + git push origin HEAD:$TRAVIS_BRANCH + notify_on_error_exit "MIRROR: Unsuccessful push, stopping..." +fi + +if [ "$TRAVIS_BRANCH" == 'integration' ] || [ "$TRAVIS_BRANCH" == 'master' ] || [ "$TRAVIS_BRANCH" == 'desktop' ] ; then + print_title "Mirror repository" + git remote add mirror https://$GIT_TOKEN@github.com/$GIT_ORG_PRIVATE/moodleapp.git + git fetch -q --unshallow mirror + notify_on_error_exit "MIRROR: Unsuccessful fetch of mirror, stopping..." + git fetch -q origin --depth=100 + notify_on_error_exit "MIRROR: Unsuccessful fetch of origin, stopping..." + git push -f mirror HEAD:$TRAVIS_BRANCH + notify_on_error_exit "MIRROR: Unsuccessful mirror, stopping..." + git push -f mirror --tags + notify_on_error_exit "MIRROR: Unsuccessful mirror tags, stopping..." +fi diff --git a/scripts/test_e2e.sh b/scripts/test_e2e.sh new file mode 100755 index 000000000..22edff551 --- /dev/null +++ b/scripts/test_e2e.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +source "scripts/functions.sh" + +# Prepare variables +basedir="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../" && pwd )" +dockerscripts="$HOME/moodle-docker/bin/" +dockercompose="$dockerscripts/moodle-docker-compose" + +export MOODLE_DOCKER_DB=pgsql +export MOODLE_DOCKER_BROWSER=chrome +export MOODLE_DOCKER_WWWROOT="$HOME/moodle" +export MOODLE_DOCKER_PHP_VERSION=7.3 +export MOODLE_DOCKER_APP_PATH=$basedir + +# Prepare dependencies +print_title "Preparing dependencies" +git clone --branch master --depth 1 git://github.com/moodle/moodle $HOME/moodle +git clone --branch master --depth 1 git://github.com/moodlehq/moodle-local_moodlemobileapp $HOME/moodle/local/moodlemobileapp +# git clone --branch master --depth 1 git://github.com/moodlehq/moodle-docker $HOME/moodle-docker + +# TODO replace with commented line above once https://github.com/moodlehq/moodle-docker/pull/126 is merged +mkdir $HOME/moodle-docker +cd $HOME/moodle-docker +git init +git remote add origin git://github.com/moodlehq/moodle-docker +git fetch --depth 1 origin c604d5f9792c72fb9d83f6fec1f4b1defd778e9a +git checkout FETCH_HEAD +cd - + +cp $HOME/moodle-docker/config.docker-template.php $HOME/moodle/config.php + +# Build app +print_title "Building app" +npm install +npm run setup + +# Start containers +print_title "Starting containers" +$dockercompose pull +$dockercompose up -d +$dockerscripts/moodle-docker-wait-for-db +$dockerscripts/moodle-docker-wait-for-app + +$dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/init.php" +notify_on_error_exit "e2e failed initializing behat" + +print_title "Running e2e tests" + +# Run tests +for tags in "$@" +do + $dockercompose exec -T webserver sh -c "php admin/tool/behat/cli/run.php --tags=\"$tags\"" + notify_on_error_exit "Some e2e tests are failing, please review" +done + +# Clean up +$dockercompose down diff --git a/scripts/ws_to_ts_functions.php b/scripts/ws_to_ts_functions.php new file mode 100644 index 000000000..ca05d2082 --- /dev/null +++ b/scripts/ws_to_ts_functions.php @@ -0,0 +1,244 @@ +. + +/** + * Helper functions for converting a Moodle WS structure to a TS type. + */ + +/** + * Get the structure of a WS params or returns. + */ +function get_ws_structure($wsname, $useparams) { + global $DB; + + // get all the function descriptions + $functions = $DB->get_records('external_functions', array(), 'name'); + $functiondescs = array(); + foreach ($functions as $function) { + $functiondescs[$function->name] = external_api::external_function_info($function); + } + + if (!isset($functiondescs[$wsname])) { + return false; + } else if ($useparams) { + return $functiondescs[$wsname]->parameters_desc; + } else { + return $functiondescs[$wsname]->returns_desc; + } +} + +/** + * Fix a comment: make sure first letter is uppercase and add a dot at the end if needed. + */ +function fix_comment($desc) { + $desc = trim($desc); + $desc = ucfirst($desc); + + if (substr($desc, -1) !== '.') { + $desc .= '.'; + } + + return $desc; +} + +/** + * Get an inline comment based on a certain text. + */ +function get_inline_comment($desc) { + if (empty($desc)) { + return ''; + } + + return ' // ' . fix_comment($desc); +} + +/** + * Add the TS documentation of a certain element. + */ +function get_ts_doc($type, $desc, $indentation) { + if (empty($desc)) { + // If no key, it's probably in an array. We only document object properties. + return ''; + } + + return $indentation . "/**\n" . + $indentation . " * " . fix_comment($desc) . "\n" . + (!empty($type) ? ($indentation . " * @type {" . $type . "}\n") : '') . + $indentation . " */\n"; +} + +/** + * Specify a certain type, with or without a key. + */ +function convert_key_type($key, $type, $required, $indentation) { + if ($key) { + // It has a key, it's inside an object. + return $indentation . "$key" . ($required == VALUE_OPTIONAL ? '?' : '') . ": $type"; + } else { + // No key, it's probably in an array. Just include the type. + return $type; + } +} + +/** + * Convert a certain element into a TS structure. + */ +function convert_to_ts($key, $value, $boolisnumber = false, $indentation = '', $arraydesc = '') { + if ($value instanceof external_value || $value instanceof external_warnings || $value instanceof external_files) { + // It's a basic field or a pre-defined type like warnings. + $type = 'string'; + + if ($value instanceof external_warnings) { + $type = 'CoreWSExternalWarning[]'; + } else if ($value instanceof external_files) { + $type = 'CoreWSExternalFile[]'; + } else if ($value->type == PARAM_BOOL && !$boolisnumber) { + $type = 'boolean'; + } else if (($value->type == PARAM_BOOL && $boolisnumber) || $value->type == PARAM_INT || $value->type == PARAM_FLOAT || + $value->type == PARAM_LOCALISEDFLOAT || $value->type == PARAM_PERMISSION || $value->type == PARAM_INTEGER || + $value->type == PARAM_NUMBER) { + $type = 'number'; + } + + $result = convert_key_type($key, $type, $value->required, $indentation); + + return $result; + + } else if ($value instanceof external_single_structure) { + // It's an object. + $result = convert_key_type($key, '{', $value->required, $indentation); + + if ($arraydesc) { + // It's an array of objects. Print the array description now. + $result .= get_inline_comment($arraydesc); + } + + $result .= "\n"; + + foreach ($value->keys as $key => $value) { + $result .= convert_to_ts($key, $value, $boolisnumber, $indentation . ' ') . ';'; + + if (!$value instanceof external_multiple_structure || !$value->content instanceof external_single_structure) { + // Add inline comments after the field, except for arrays of objects where it's added at the start. + $result .= get_inline_comment($value->desc); + } + + $result .= "\n"; + } + + $result .= "$indentation}"; + + return $result; + + } else if ($value instanceof external_multiple_structure) { + // It's an array. + $result = convert_key_type($key, '', $value->required, $indentation); + + $result .= convert_to_ts(null, $value->content, $boolisnumber, $indentation, $value->desc); + + $result .= "[]"; + + return $result; + } else { + echo "WARNING: Unknown structure: $key " . get_class($value) . " \n"; + + return ""; + } +} + +/** + * Concatenate two paths. + */ +function concatenate_paths($left, $right, $separator = '/') { + if (!is_string($left) || $left == '') { + return $right; + } else if (!is_string($right) || $right == '') { + return $left; + } + + $lastCharLeft = substr($left, -1); + $firstCharRight = $right[0]; + + if ($lastCharLeft === $separator && $firstCharRight === $separator) { + return $left . substr($right, 1); + } else if ($lastCharLeft !== $separator && $firstCharRight !== '/') { + return $left . '/' . $right; + } else { + return $left . $right; + } +} + +/** + * Detect changes between 2 WS structures. We only detect fields that have been added or modified, not removed fields. + */ +function detect_ws_changes($new, $old, $key = '', $path = '') { + $messages = []; + + if (gettype($new) != gettype($old)) { + // The type has changed. + $messages[] = "Property '$key' has changed type, from '" . gettype($old) . "' to '" . gettype($new) . + ($path != '' ? "' inside $path." : "'."); + + } else if ($new instanceof external_value && $new->type != $old->type) { + // The type has changed. + $messages[] = "Property '$key' has changed type, from '" . $old->type . "' to '" . $new->type . + ($path != '' ? "' inside $path." : "'."); + + } else if ($new instanceof external_warnings || $new instanceof external_files) { + // Ignore these types. + + } else if ($new instanceof external_single_structure) { + // Check each subproperty. + $newpath = ($path != '' ? "$path." : '') . $key; + + foreach ($new->keys as $subkey => $value) { + if (!isset($old->keys[$subkey])) { + // New property. + $messages[] = "New property '$subkey' found" . ($newpath != '' ? " inside '$newpath'." : '.'); + } else { + $messages = array_merge($messages, detect_ws_changes($value, $old->keys[$subkey], $subkey, $newpath)); + } + } + } else if ($new instanceof external_multiple_structure) { + // Recursive call with the content. + $messages = array_merge($messages, detect_ws_changes($new->content, $old->content, $key, $path)); + } + + return $messages; +} + +/** + * Remove all closures (anonymous functions) in the default values so the object can be serialized. + */ +function remove_default_closures($value) { + if ($value instanceof external_warnings || $value instanceof external_files) { + // Ignore these types. + + } else if ($value instanceof external_value) { + if ($value->default instanceof Closure) { + $value->default = null; + } + + } else if ($value instanceof external_single_structure) { + + foreach ($value->keys as $key => $subvalue) { + remove_default_closures($subvalue); + } + + } else if ($value instanceof external_multiple_structure) { + remove_default_closures($value->content); + } +} diff --git a/src/addon/block/activityresults/activityresults.module.ts b/src/addon/block/activityresults/activityresults.module.ts new file mode 100644 index 000000000..3c0ee74d3 --- /dev/null +++ b/src/addon/block/activityresults/activityresults.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreBlockDelegate } from '@core/block/providers/delegate'; +import { AddonBlockActivityResultsHandler } from './providers/block-handler'; + +@NgModule({ + declarations: [ + ], + imports: [ + IonicModule, + TranslateModule.forChild() + ], + exports: [ + ], + providers: [ + AddonBlockActivityResultsHandler + ] +}) +export class AddonBlockActivityResultsModule { + constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockActivityResultsHandler) { + blockDelegate.registerHandler(blockHandler); + } +} diff --git a/src/addon/block/activityresults/activityresults.scss b/src/addon/block/activityresults/activityresults.scss new file mode 100644 index 000000000..b76cb85a9 --- /dev/null +++ b/src/addon/block/activityresults/activityresults.scss @@ -0,0 +1,31 @@ +.addon-block-activity-results core-block-pre-rendered { + ion-item.core-block-content { + table.grades { + @include text-align('start'); + width: 100%; + + .number { + @include text-align('start'); + width: 10%; + } + + .name { + @include text-align('start'); + width: 77%; + } + + .grade { + @include text-align('end'); + } + + caption { + @include text-align('start'); + padding-top: .75rem; + padding-bottom: .75rem; + color: $gray-darker; + font-weight: bold; + font-size: 18px; + } + } + } +} \ No newline at end of file diff --git a/src/addon/block/activityresults/lang/en.json b/src/addon/block/activityresults/lang/en.json new file mode 100644 index 000000000..4dd6abbb7 --- /dev/null +++ b/src/addon/block/activityresults/lang/en.json @@ -0,0 +1,3 @@ +{ + "pluginname": "Activity results" +} \ No newline at end of file diff --git a/src/addon/block/activityresults/providers/block-handler.ts b/src/addon/block/activityresults/providers/block-handler.ts new file mode 100644 index 000000000..8553cb3a4 --- /dev/null +++ b/src/addon/block/activityresults/providers/block-handler.ts @@ -0,0 +1,51 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, Injector } from '@angular/core'; + +import { CoreBlockHandlerData } from '@core/block/providers/delegate'; +import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block'; +import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler'; + +/** + * Block handler. + */ +@Injectable() +export class AddonBlockActivityResultsHandler extends CoreBlockBaseHandler { + name = 'AddonBlockActivityResults'; + blockName = 'activity_results'; + + constructor() { + super(); + } + + /** + * Returns the data needed to render the block. + * + * @param injector Injector. + * @param block The block to render. + * @param contextLevel The context where the block will be used. + * @param instanceId The instance ID associated with the context level. + * @return Data or promise resolved with the data. + */ + getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number) + : CoreBlockHandlerData | Promise { + + return { + title: 'addon.block_activityresults.pluginname', + class: 'addon-block-activity-results', + component: CoreBlockPreRenderedComponent + }; + } +} diff --git a/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html index e4016144a..4b7495bfa 100644 --- a/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -11,7 +11,8 @@ - + + diff --git a/src/addon/block/myoverview/components/myoverview/myoverview.ts b/src/addon/block/myoverview/components/myoverview/myoverview.ts index 0f3cce93b..074b176b9 100644 --- a/src/addon/block/myoverview/components/myoverview/myoverview.ts +++ b/src/addon/block/myoverview/components/myoverview/myoverview.ts @@ -17,7 +17,7 @@ import { Searchbar } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData } from '@core/courses/providers/courses'; import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; @@ -83,6 +83,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem protected updateSiteObserver; protected courseIds = []; protected fetchContentDefaultError = 'Error getting my overview data.'; + protected showSortByShortName = false; constructor(injector: Injector, protected coursesProvider: CoreCoursesProvider, @@ -112,8 +113,12 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem }, this.sitesProvider.getCurrentSiteId()); - this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => { - this.refreshContent(); + this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, + (data: CoreCoursesMyCoursesUpdatedEventData) => { + + if (data.action == CoreCoursesProvider.ACTION_ENROL || data.action == CoreCoursesProvider.ACTION_STATE_CHANGED) { + this.refreshCourseList(); + } }, this.sitesProvider.getCurrentSiteId()); this.currentSite = this.sitesProvider.getCurrentSite(); @@ -151,12 +156,9 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem promises.push(this.coursesProvider.invalidateUserCourses().finally(() => { // Invalidate course completion data. - promises.push(this.coursesProvider.invalidateUserCourses().finally(() => { - // Invalidate course completion data. - return this.utils.allPromises(this.courseIds.map((courseId) => { - return this.courseCompletionProvider.invalidateCourseCompletion(courseId); - })); - })); + return this.utils.allPromises(this.courseIds.map((courseId) => { + return this.courseCompletionProvider.invalidateCourseCompletion(courseId); + })); })); promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions()); @@ -180,6 +182,18 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem const showCategories = config && config.displaycategories && config.displaycategories.value == '1'; return this.coursesHelper.getUserCoursesWithOptions(this.sort, null, null, showCategories).then((courses) => { + // Check to show sort by short name only if the text is visible. + if (courses.length > 0) { + const sampleCourse = courses[0]; + this.showSortByShortName = sampleCourse.displayname && sampleCourse.shortname && + sampleCourse.fullname != sampleCourse.displayname; + } + + // Rollback to sort by full name if user is sorting by short name then Moodle web change the config. + if (!this.showSortByShortName && this.sort === 'shortname') { + this.switchSort('fullname'); + } + this.courseIds = courses.map((course) => { return course.id; }); @@ -318,6 +332,23 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem }); } + /** + * Refresh the list of courses. + * + * @return Promise resolved when done. + */ + protected async refreshCourseList(): Promise { + this.eventsProvider.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); + + try { + await this.coursesProvider.invalidateUserCourses(); + } catch (error) { + // Ignore errors. + } + + await this.loadContent(true); + } + /** * The selected courses filter have changed. */ @@ -377,7 +408,7 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem this.courses.allincludinghidden = courses; if (this.showSortFilter) { - if (this.sort == 'lastaccess') { + if (this.sort == 'lastaccess') { courses.sort((a, b) => { return b.lastaccess - a.lastaccess; }); @@ -386,6 +417,14 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem const compareA = a.fullname.toLowerCase(), compareB = b.fullname.toLowerCase(); + return compareA.localeCompare(compareB); + }); + } else if (this.sort == 'shortname') { + courses.sort((a, b) => { + const compareA = a.shortname.toLowerCase(), + compareB = b.shortname.toLowerCase(); + compareA.localeCompare(); + return compareA.localeCompare(compareB); }); } diff --git a/src/addon/block/myoverview/lang/en.json b/src/addon/block/myoverview/lang/en.json index 4c9ca27af..9d38164d8 100644 --- a/src/addon/block/myoverview/lang/en.json +++ b/src/addon/block/myoverview/lang/en.json @@ -10,5 +10,6 @@ "nocourses": "No courses", "past": "Past", "pluginname": "Course overview", + "shortname": "Short name", "title": "Course name" -} \ No newline at end of file +} diff --git a/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts index 4b61bfeb1..a1eb249ce 100644 --- a/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts +++ b/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts @@ -15,7 +15,7 @@ import { Component, OnInit, OnDestroy, Injector, Input, OnChanges, SimpleChange } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData } from '@core/courses/providers/courses'; import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; @@ -72,8 +72,12 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom }, this.sitesProvider.getCurrentSiteId()); - this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => { - this.refreshContent(); + this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, + (data: CoreCoursesMyCoursesUpdatedEventData) => { + + if (this.shouldRefreshOnUpdatedEvent(data)) { + this.refreshCourseList(); + } }, this.sitesProvider.getCurrentSiteId()); super.ngOnInit(); @@ -130,6 +134,23 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom }); } + /** + * Refresh the list of courses. + * + * @return Promise resolved when done. + */ + protected async refreshCourseList(): Promise { + this.eventsProvider.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); + + try { + await this.coursesProvider.invalidateUserCourses(); + } catch (error) { + // Ignore errors. + } + + await this.loadContent(true); + } + /** * Initialize the prefetch icon for selected courses. */ @@ -146,6 +167,49 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom }); } + /** + * Whether list should be refreshed 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 != this.sitesProvider.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 && + 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) => { + return course.id == courseId; + }); + } + /** * Prefetch all the shown courses. * diff --git a/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts b/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts index 8acddb2a2..0fc513209 100644 --- a/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts +++ b/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts @@ -15,7 +15,7 @@ import { Component, OnInit, OnDestroy, Injector, Input, OnChanges, SimpleChange } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; -import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData } from '@core/courses/providers/courses'; import { CoreCoursesHelperProvider } from '@core/courses/providers/helper'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate'; @@ -72,7 +72,12 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im }, this.sitesProvider.getCurrentSiteId()); - this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => { + this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, + (data: CoreCoursesMyCoursesUpdatedEventData) => { + + if (this.shouldRefreshOnUpdatedEvent(data)) { + this.refreshCourseList(); + } this.refreshContent(); }, this.sitesProvider.getCurrentSiteId()); @@ -130,6 +135,44 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im }); } + /** + * Refresh the list of courses. + * + * @return Promise resolved when done. + */ + protected async refreshCourseList(): Promise { + this.eventsProvider.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); + + try { + await this.coursesProvider.invalidateUserCourses(); + } catch (error) { + // Ignore errors. + } + + await this.loadContent(true); + } + + /** + * Whether list should be refreshed 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. + // New courses shouldn't be favourite by default, but just in case. + return true; + } + + 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. */ diff --git a/src/addon/block/timeline/components/events/addon-block-timeline-events.html b/src/addon/block/timeline/components/events/addon-block-timeline-events.html index 832c4902d..6b983e107 100644 --- a/src/addon/block/timeline/components/events/addon-block-timeline-events.html +++ b/src/addon/block/timeline/components/events/addon-block-timeline-events.html @@ -5,8 +5,8 @@ -

-

+

+

diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts index 8853d8842..50b71f10c 100644 --- a/src/addon/blog/components/entries/entries.ts +++ b/src/addon/blog/components/entries/entries.ts @@ -16,6 +16,7 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreTextUtils } from '@providers/utils/text'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUserProvider } from '@core/user/providers/user'; import { AddonBlogProvider, AddonBlogPost } from '../../providers/blog'; @@ -162,6 +163,8 @@ export class AddonBlogEntriesComponent implements OnInit { entry.contextInstanceId = entry.userid; } + entry.summary = CoreTextUtils.instance.replacePluginfileUrls(entry.summary, entry.summaryfiles || []); + return this.userProvider.getProfile(entry.userid, entry.courseid, true).then((user) => { entry.user = user; }).catch(() => { diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts index 5de3c1fe2..e68d4b362 100644 --- a/src/addon/calendar/pages/edit-event/edit-event.ts +++ b/src/addon/calendar/pages/edit-event/edit-event.ts @@ -458,7 +458,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy { eventtype: formData.eventtype, timestart: timeStartDate, description: { - text: formData.description, + text: formData.description || '', format: 1 }, location: formData.location, diff --git a/src/addon/messages/pages/discussion/discussion.html b/src/addon/messages/pages/discussion/discussion.html index a869dad74..fbe0c89b3 100644 --- a/src/addon/messages/pages/discussion/discussion.html +++ b/src/addon/messages/pages/discussion/discussion.html @@ -41,7 +41,7 @@ - {{ 'addon.messages.newmessages' | translate:{$a: title} }} + {{ 'addon.messages.newmessages' | translate }} @@ -68,6 +68,13 @@
+ + + +
diff --git a/src/addon/messages/pages/discussion/discussion.scss b/src/addon/messages/pages/discussion/discussion.scss index 0c2237fc2..4b323f98f 100644 --- a/src/addon/messages/pages/discussion/discussion.scss +++ b/src/addon/messages/pages/discussion/discussion.scss @@ -4,6 +4,9 @@ $item-message-note-text: $gray-dark !default; $item-message-note-font-size: 75% !default; $item-message-mine-bg: $gray-light !default; +$core-discussion-messages-badge: $core-color !default; +$core-discussion-messages-badge-text: $white !default; + @mixin message-page { ion-content { background-color: $gray-lighter !important; @@ -194,6 +197,30 @@ $item-message-mine-bg: $gray-light !default; border-top-right-radius: 0; border-top-left-radius: 0; } + + .has-fab .scroll-content { + padding-bottom: 0; + } + ion-fab button { + overflow: visible; + position: relative; + box-shadow: $fab-md-box-shadow; + + .core-discussion-messages-badge { + position: absolute; + border-radius: 50%; + color: $core-discussion-messages-badge-text; + background-color: $core-discussion-messages-badge; + display: block; + line-height: 20px; + height: 20px; + width: 20px; + right: -6px; + top: -6px; + + } + } + } diff --git a/src/addon/messages/pages/discussion/discussion.ts b/src/addon/messages/pages/discussion/discussion.ts index a5b79b15f..043376f1d 100644 --- a/src/addon/messages/pages/discussion/discussion.ts +++ b/src/addon/messages/pages/discussion/discussion.ts @@ -65,6 +65,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { protected viewDestroyed = false; protected memberInfoObserver: any; protected showLoadingModal = false; // Whether to show a loading modal while fetching data. + protected scrollListener; conversationId: number; // Conversation ID. Undefined if it's a new individual conversation. conversation: AddonMessagesConversationFormatted; // The conversation object (if it exists). @@ -95,6 +96,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { isSelf = false; muteEnabled = false; muteIcon = 'volume-off'; + newMessages = 0; constructor(private eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider, navParams: NavParams, private userProvider: CoreUserProvider, private navCtrl: NavController, private messagesSync: AddonMessagesSyncProvider, @@ -134,6 +136,8 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.fetchData(); } }, this.siteId); + + this.scrollListener = this.scrollListenerFunction.bind(this); } /** @@ -141,21 +145,26 @@ export class AddonMessagesDiscussionPage implements OnDestroy { * * @param message Message to be added. * @param keep If set the keep flag or not. + * @return If message is not mine and was recently added. */ protected addMessage(message: AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted, - keep: boolean = true): void { + keep: boolean = true): boolean { /* Create a hash to identify the message. The text of online messages isn't reliable because it can have random data like VideoJS ID. Try to use id and fallback to text for offline messages. */ message.hash = Md5.hashAsciiStr(String(message.id || message.text || '')) + '#' + message.timecreated + '#' + message.useridfrom; + let added = false; if (typeof this.keepMessageMap[message.hash] === 'undefined') { // Message not added to the list. Add it now. this.messages.push(message); + added = message.useridfrom != this.currentUserId; } // Message needs to be kept in the list. this.keepMessageMap[message.hash] = keep; + + return added; } /** @@ -306,9 +315,10 @@ export class AddonMessagesDiscussionPage implements OnDestroy { /** * Convenience function to fetch messages. * + * @param messagesAreNew If messages loaded are new messages. * @return Resolved when done. */ - protected fetchMessages(): Promise { + protected fetchMessages(messagesAreNew: boolean = true): Promise { this.loadMoreError = false; if (this.messagesBeingSent > 0) { @@ -348,7 +358,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { }); } }).then((messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[]) => { - this.loadMessages(messages); + this.loadMessages(messages, messagesAreNew); }).finally(() => { this.fetching = false; }); @@ -357,10 +367,11 @@ export class AddonMessagesDiscussionPage implements OnDestroy { /** * Format and load a list of messages into the view. * + * @param messagesAreNew If messages loaded are new messages. * @param messages Messages to load. */ - protected loadMessages(messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[]) - : void { + protected loadMessages(messages: (AddonMessagesConversationMessageFormatted | AddonMessagesGetMessagesMessageFormatted)[], + messagesAreNew: boolean = true): void { if (this.viewDestroyed) { return; @@ -380,9 +391,14 @@ export class AddonMessagesDiscussionPage implements OnDestroy { } // Add new messages to the list and mark the messages that should still be displayed. - messages.forEach((message) => { - this.addMessage(message); - }); + const newMessages = messages.reduce((val, message) => { + return val + (this.addMessage(message) ? 1 : 0); + }, 0); + + // Set the new badges message if we're loading new messages. + if (messagesAreNew) { + this.setNewMessagesBadge(this.newMessages + newMessages); + } // Remove messages that shouldn't be in the list anymore. for (const hash in this.keepMessageMap) { @@ -414,6 +430,63 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.markMessagesAsRead(forceMark); } + /** + * Set the new message badge number and set scroll listener if needed. + * + * @param addMessages NUmber of messages still to be read. + */ + protected setNewMessagesBadge(addMessages: number): void { + if (this.newMessages == 0 && addMessages > 0) { + // Setup scrolling. + this.content.getScrollElement().addEventListener('scroll', this.scrollListener); + + this.scrollListenerFunction(); + } else if (this.newMessages > 0 && addMessages == 0) { + // Remove scrolling. + this.content.getScrollElement().removeEventListener('scroll', this.scrollListener); + } + + this.newMessages = addMessages; + } + + /** + * The scroll was moved. Update new messages count. + */ + protected scrollListenerFunction(): void { + if (this.newMessages > 0) { + const scrollBottom = this.domUtils.getScrollTop(this.content) + this.domUtils.getContentHeight(this.content); + const scrollHeight = this.domUtils.getScrollHeight(this.content); + if (scrollBottom > scrollHeight - 40) { + // At the bottom, reset. + this.setNewMessagesBadge(0); + + return; + } + + const scrollElRect = this.content.getScrollElement().getBoundingClientRect(); + const scrollBottomPos = (scrollElRect && scrollElRect.bottom) || 0; + + if (scrollBottomPos == 0) { + return; + } + + const messages = Array.from(document.querySelectorAll('.addon-message-not-mine')).slice(-this.newMessages).reverse(); + + const newMessagesUnread = messages.findIndex((message, index) => { + const elementRect = message.getBoundingClientRect(); + if (!elementRect) { + return false; + } + + return elementRect.bottom <= scrollBottomPos; + }); + + if (newMessagesUnread > 0 && newMessagesUnread < this.newMessages) { + this.setNewMessagesBadge(newMessagesUnread); + } + } + } + /** * Get the conversation. * @@ -887,7 +960,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { return this.waitForFetch().finally(() => { this.pagesLoaded++; - this.fetchMessages().then(() => { + this.fetchMessages(false).then(() => { // Try to keep the scroll position. const scrollBottom = scrollHeight - this.domUtils.getScrollTop(this.content); @@ -972,6 +1045,20 @@ export class AddonMessagesDiscussionPage implements OnDestroy { } }); this.scrollBottom = false; + + // Reset the badge. + this.setNewMessagesBadge(0); + } + } + + /** + * Scroll to the first new unread message. + */ + scrollToFirstUnreadMessage(): void { + if (this.newMessages > 0) { + const messages = Array.from(document.querySelectorAll('.addon-message-not-mine')); + + this.domUtils.scrollToElement(this.content, messages[messages.length - this.newMessages]); } } @@ -987,6 +1074,7 @@ export class AddonMessagesDiscussionPage implements OnDestroy { this.showDelete = false; this.scrollBottom = true; + this.setNewMessagesBadge(0); message = { id: null, diff --git a/src/addon/mod/assign/components/index/addon-mod-assign-index.html b/src/addon/mod/assign/components/index/addon-mod-assign-index.html index 467f9e49d..ee087a9df 100644 --- a/src/addon/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addon/mod/assign/components/index/addon-mod-assign-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts index b392abef3..8d8fb9985 100644 --- a/src/addon/mod/assign/components/index/index.ts +++ b/src/addon/mod/assign/components/index/index.ts @@ -133,8 +133,15 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo ev && ev.stopPropagation(); if (this.assign && (this.description || this.assign.introattachments)) { - this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component, - this.module.id, this.assign.introattachments, true, 'module', this.module.id, this.courseId); + this.textUtils.viewText(this.translate.instant('core.description'), this.description, { + component: this.component, + componentId: this.module.id, + files: this.assign.introattachments, + filter: true, + contextLevel: 'module', + instanceId: this.module.id, + courseId: this.courseId, + }); } } diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts index 9c231cabf..6bc9c7282 100644 --- a/src/addon/mod/assign/components/submission/submission.ts +++ b/src/addon/mod/assign/components/submission/submission.ts @@ -678,8 +678,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy { */ showAdvancedGrade(): void { if (this.feedback && this.feedback.advancedgrade) { - this.textUtils.expandText(this.translate.instant('core.grades.grade'), this.feedback.gradefordisplay, - AddonModAssignProvider.COMPONENT, this.moduleId); + this.textUtils.viewText(this.translate.instant('core.grades.grade'), this.feedback.gradefordisplay, { + component: AddonModAssignProvider.COMPONENT, + componentId: this.moduleId, + }); } } diff --git a/src/addon/mod/assign/feedback/comments/component/comments.ts b/src/addon/mod/assign/feedback/comments/component/comments.ts index 27eb0baa1..1207e8f33 100644 --- a/src/addon/mod/assign/feedback/comments/component/comments.ts +++ b/src/addon/mod/assign/feedback/comments/component/comments.ts @@ -65,8 +65,14 @@ export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedb if (this.text) { // Open a new state with the text. - this.textUtils.expandText(this.plugin.name, this.text, this.component, this.assign.cmid, undefined, true, - 'module', this.assign.cmid, this.assign.course); + this.textUtils.viewText(this.plugin.name, this.text, { + component: this.component, + componentId: this.assign.cmid, + filter: true, + contextLevel: 'module', + instanceId: this.assign.cmid, + courseId: this.assign.course, + }); } }); } else if (this.edit) { diff --git a/src/addon/mod/assign/pages/edit/edit.ts b/src/addon/mod/assign/pages/edit/edit.ts index 87eb9ee29..33fcf85b8 100644 --- a/src/addon/mod/assign/pages/edit/edit.ts +++ b/src/addon/mod/assign/pages/edit/edit.ts @@ -317,6 +317,10 @@ export class AddonModAssignEditPage implements OnInit, OnDestroy { // Clear temporary data from plugins. await this.assignHelper.clearSubmissionPluginTmpData(this.assign, this.userSubmission, inputData); + if (sent) { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'assign' }); + } + // Submission saved, trigger events. this.domUtils.triggerFormSubmittedEvent(this.formElement, sent, this.sitesProvider.getCurrentSiteId()); diff --git a/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts index a788de667..ccfb0d3cd 100644 --- a/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts +++ b/src/addon/mod/assign/submission/onlinetext/component/onlinetext.ts @@ -83,8 +83,14 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS if (text) { // Open a new state with the interpolated contents. - this.textUtils.expandText(this.plugin.name, text, this.component, this.assign.cmid, undefined, true, - 'module', this.assign.cmid, this.assign.course); + this.textUtils.viewText(this.plugin.name, text, { + component: this.component, + componentId: this.assign.cmid, + filter: true, + contextLevel: 'module', + instanceId: this.assign.cmid, + courseId: this.assign.course, + }); } }); } else { diff --git a/src/addon/mod/book/components/index/addon-mod-book-index.html b/src/addon/mod/book/components/index/addon-mod-book-index.html index 6fcc9f70d..ee1e5bbad 100644 --- a/src/addon/mod/book/components/index/addon-mod-book-index.html +++ b/src/addon/mod/book/components/index/addon-mod-book-index.html @@ -9,7 +9,7 @@ - + @@ -18,6 +18,10 @@ + + + +
diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 50c69e8bd..b52ed71cb 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -14,13 +14,12 @@ import { Component, Optional, Injector, Input } from '@angular/core'; import { Content, ModalController } from 'ionic-angular'; -import { CoreAppProvider } from '@providers/app'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; +import { + CoreCourseModuleMainResourceComponent, CoreCourseResourceDownloadResult +} from '@core/course/classes/main-resource-component'; import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter, AddonModBookBook, AddonModBookNavStyle } from '../../providers/book'; -import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler'; import { CoreTagProvider } from '@core/tag/providers/tag'; /** @@ -41,6 +40,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp displayNavBar = true; previousNavBarTitle: string; nextNavBarTitle: string; + warning: string; protected chapters: AddonModBookTocChapter[]; protected currentChapter: string; @@ -48,9 +48,11 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp protected book: AddonModBookBook; protected displayTitlesInNavBar = false; - constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider, - private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler, - private modalCtrl: ModalController, private tagProvider: CoreTagProvider, @Optional() private content: Content) { + constructor(injector: Injector, + protected bookProvider: AddonModBookProvider, + protected modalCtrl: ModalController, + protected tagProvider: CoreTagProvider, + @Optional() protected content: Content) { super(injector); } @@ -126,8 +128,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp */ protected fetchContent(refresh?: boolean): Promise { const promises = []; - let downloadFailed = false; - let downloadFailError; + let downloadResult: CoreCourseResourceDownloadResult; // Try to get the book data. promises.push(this.bookProvider.getBook(this.courseId, this.module.id).then((book) => { @@ -140,16 +141,9 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp // Ignore errors since this WS isn't available in some Moodle versions. })); - // Download content. This function also loads module contents if needed. - promises.push(this.prefetchDelegate.download(this.module, this.courseId).catch((error) => { - // Mark download as failed but go on since the main files could have been downloaded. - downloadFailed = true; - downloadFailError = error; - - if (!this.module.contents.length) { - // Try to load module contents for offline usage. - return this.courseProvider.loadModuleContents(this.module, this.courseId); - } + // Get module status to determine if it needs to be downloaded. + promises.push(this.downloadResourceIfNeeded(refresh).then((result) => { + downloadResult = result; })); return Promise.all(promises).then(() => { @@ -174,10 +168,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp // Show chapter. return this.loadChapter(this.currentChapter, refresh).then(() => { - if (downloadFailed && this.appProvider.isOnline()) { - // We could load the main file but the download failed. Show error message. - this.showErrorDownloadingSomeFiles(downloadFailError); - } + this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error) : ''; }).catch(() => { // Ignore errors, they're handled inside the loadChapter function. }); diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts index bd34a6644..3130f397c 100644 --- a/src/addon/mod/chat/pages/chat/chat.ts +++ b/src/addon/mod/chat/pages/chat/chat.ts @@ -117,6 +117,7 @@ export class AddonModChatChatPage { * Runs when the page is about to leave and no longer be the active page. */ ionViewWillLeave(): void { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'chat' }); this.stopPolling(); } diff --git a/src/addon/mod/choice/components/index/addon-mod-choice-index.html b/src/addon/mod/choice/components/index/addon-mod-choice-index.html index 762ad5066..bc5de5829 100644 --- a/src/addon/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addon/mod/choice/components/index/addon-mod-choice-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/choice/components/index/index.ts b/src/addon/mod/choice/components/index/index.ts index 6423f5439..2ce8f309b 100644 --- a/src/addon/mod/choice/components/index/index.ts +++ b/src/addon/mod/choice/components/index/index.ts @@ -14,6 +14,7 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; +import { CoreEvents, CoreEventsProvider } from '@providers/events'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { AddonModChoiceProvider, AddonModChoiceChoice, AddonModChoiceOption, AddonModChoiceResult } from '../../providers/choice'; @@ -51,9 +52,14 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo protected hasAnsweredOnline = false; protected now: number; - constructor(injector: Injector, private choiceProvider: AddonModChoiceProvider, @Optional() content: Content, - private choiceOffline: AddonModChoiceOfflineProvider, private choiceSync: AddonModChoiceSyncProvider, - private timeUtils: CoreTimeUtilsProvider) { + constructor( + injector: Injector, + protected choiceProvider: AddonModChoiceProvider, + @Optional() content: Content, + protected choiceOffline: AddonModChoiceOfflineProvider, + protected choiceSync: AddonModChoiceSyncProvider, + protected timeUtils: CoreTimeUtilsProvider, + ) { super(injector, content); } @@ -359,6 +365,10 @@ export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityCo this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); this.domUtils.scrollToTop(this.content); + if (online) { + CoreEvents.instance.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: this.moduleName }); + } + return this.dataUpdated(online); }).catch((message) => { this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true); diff --git a/src/addon/mod/data/components/index/addon-mod-data-index.html b/src/addon/mod/data/components/index/addon-mod-data-index.html index 200fb4387..af4a82ad6 100644 --- a/src/addon/mod/data/components/index/addon-mod-data-index.html +++ b/src/addon/mod/data/components/index/addon-mod-data-index.html @@ -12,7 +12,7 @@ - + diff --git a/src/addon/mod/data/fields/url/component/addon-mod-data-field-url.html b/src/addon/mod/data/fields/url/component/addon-mod-data-field-url.html index 90283fe72..7d2c37bec 100644 --- a/src/addon/mod/data/fields/url/component/addon-mod-data-field-url.html +++ b/src/addon/mod/data/fields/url/component/addon-mod-data-field-url.html @@ -4,4 +4,7 @@ -{{value.content}} \ No newline at end of file + + {{ displayValue }} + {{ displayValue }} + \ No newline at end of file diff --git a/src/addon/mod/data/fields/url/component/url.ts b/src/addon/mod/data/fields/url/component/url.ts index 14dd7f62b..175f12096 100644 --- a/src/addon/mod/data/fields/url/component/url.ts +++ b/src/addon/mod/data/fields/url/component/url.ts @@ -24,6 +24,9 @@ import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin- }) export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginComponent { + protected autoLink = false; + protected displayValue = ''; + constructor(protected fb: FormBuilder) { super(fb); } @@ -43,4 +46,36 @@ export class AddonModDataFieldUrlComponent extends AddonModDataFieldPluginCompon this.addControl('f_' + this.field.id, value); } + + /** + * Calculate data for show or list mode. + */ + protected calculateShowListData(): void { + if (!this.value || !this.value.content) { + return; + } + + const url = this.value.content; + const text = this.field.param2 || this.value.content1; // Param2 forces the text to display. + + this.autoLink = parseInt(this.field.param1, 10) === 1; + + if (this.autoLink) { + this.displayValue = text || url; + } else { + // No auto link, always display the URL. + this.displayValue = url; + } + } + + /** + * Update value being shown. + */ + protected updateValue(value: any): void { + super.updateValue(value); + + if (this.isShowOrListMode()) { + this.calculateShowListData(); + } + } } diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts index a830e02d8..877785bbd 100644 --- a/src/addon/mod/data/pages/edit/edit.ts +++ b/src/addon/mod/data/pages/edit/edit.ts @@ -217,6 +217,10 @@ export class AddonModDataEditPage { this.domUtils.triggerFormSubmittedEvent(this.formElement, result.sent, this.siteId); + if (result.sent) { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'data' }); + } + const promises = []; this.entryId = this.entryId || result.newentryid; diff --git a/src/addon/mod/data/providers/approve-link-handler.ts b/src/addon/mod/data/providers/approve-link-handler.ts index f4c9bb5f4..7426ad39f 100644 --- a/src/addon/mod/data/providers/approve-link-handler.ts +++ b/src/addon/mod/data/providers/approve-link-handler.ts @@ -27,6 +27,7 @@ export class AddonModDataApproveLinkHandler extends CoreContentLinksHandlerBase name = 'AddonModDataApproveLinkHandler'; featureName = 'CoreCourseModuleDelegate_AddonModData'; pattern = /\/mod\/data\/view\.php.*([\?\&](d|approve|disapprove)=\d+)/; + priority = 50; // Higher priority than the default link handler for view.php. constructor(private dataProvider: AddonModDataProvider, private dataHelper: AddonModDataHelperProvider) { super(); diff --git a/src/addon/mod/data/providers/show-link-handler.ts b/src/addon/mod/data/providers/show-link-handler.ts index 5f4f3b22c..5bb471762 100644 --- a/src/addon/mod/data/providers/show-link-handler.ts +++ b/src/addon/mod/data/providers/show-link-handler.ts @@ -29,6 +29,7 @@ export class AddonModDataShowLinkHandler extends CoreContentLinksHandlerBase { name = 'AddonModDataShowLinkHandler'; featureName = 'CoreCourseModuleDelegate_AddonModData'; pattern = /\/mod\/data\/view\.php.*([\?\&](d|rid|page|group|mode)=\d+)/; + priority = 50; // Higher priority than the default link handler for view.php. constructor(private linkHelper: CoreContentLinksHelperProvider, private dataProvider: AddonModDataProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) { diff --git a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html index 4355c5310..c7edc940d 100644 --- a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html @@ -7,7 +7,7 @@ - + @@ -108,18 +108,18 @@ - - - - - diff --git a/src/addon/mod/feedback/pages/form/form.html b/src/addon/mod/feedback/pages/form/form.html index adb569d88..99b805762 100644 --- a/src/addon/mod/feedback/pages/form/form.html +++ b/src/addon/mod/feedback/pages/form/form.html @@ -67,20 +67,20 @@ - - - - - - @@ -98,14 +98,14 @@ - - - - diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts index 3ee5f6ce9..8e136401a 100644 --- a/src/addon/mod/feedback/pages/form/form.ts +++ b/src/addon/mod/feedback/pages/form/form.ts @@ -280,6 +280,8 @@ export class AddonModFeedbackFormPage implements OnDestroy { promises.push(this.feedbackProvider.invalidateFeedbackAccessInformationData(this.feedback.id)); promises.push(this.feedbackProvider.invalidateResumePageData(this.feedback.id)); + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'feedback' }); + return Promise.all(promises).then(() => { return this.fetchAccessData(); }); diff --git a/src/addon/mod/feedback/providers/prefetch-handler.ts b/src/addon/mod/feedback/providers/prefetch-handler.ts index 01c846d74..0c0562638 100644 --- a/src/addon/mod/feedback/providers/prefetch-handler.ts +++ b/src/addon/mod/feedback/providers/prefetch-handler.ts @@ -28,6 +28,7 @@ import { CoreGroupsProvider } from '@providers/groups'; import { AddonModFeedbackSyncProvider } from './sync'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; +import { CoreWSExternalFile } from '@providers/ws'; /** * Handler to prefetch feedbacks. @@ -68,26 +69,31 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH * @param single True if we're downloading a single module, false if we're downloading a whole section. * @return Promise resolved with the list of files. */ - getFiles(module: any, courseId: number, single?: boolean): Promise { + async getFiles(module: any, courseId: number, single?: boolean): Promise { let files = []; - return this.feedbackProvider.getFeedback(courseId, module.id).then((feedback) => { + const feedback = await this.feedbackProvider.getFeedback(courseId, module.id); - // Get intro files and page after submit files. - files = feedback.pageaftersubmitfiles || []; - files = files.concat(this.getIntroFilesFromInstance(module, feedback)); + // Get intro files and page after submit files. + files = feedback.pageaftersubmitfiles || []; + files = files.concat(this.getIntroFilesFromInstance(module, feedback)); + + try { + const response = await this.feedbackProvider.getItems(feedback.id); - return this.feedbackProvider.getItems(feedback.id); - }).then((response) => { response.items.forEach((item) => { - files = files.concat(item.itemfiles); + files = files.concat(item.itemfiles.map((file) => { + file.fileurl = file.fileurl || file.url; + + return file; + })); }); - return files; - }).catch(() => { - // Any error, return the list we have. - return files; - }); + } catch (e) { + // Ignore errors. + } + + return files; } /** @@ -97,7 +103,7 @@ export class AddonModFeedbackPrefetchHandler extends CoreCourseActivityPrefetchH * @param courseId Course ID. * @return Promise resolved with list of intro files. */ - getIntroFiles(module: any, courseId: number): Promise { + getIntroFiles(module: any, courseId: number): Promise { return this.feedbackProvider.getFeedback(courseId, module.id).catch(() => { // Not found, return undefined so module description is used. }).then((feedback) => { diff --git a/src/addon/mod/folder/components/index/addon-mod-folder-index.html b/src/addon/mod/folder/components/index/addon-mod-folder-index.html index 03eb3838a..2aa7976f3 100644 --- a/src/addon/mod/folder/components/index/addon-mod-folder-index.html +++ b/src/addon/mod/folder/components/index/addon-mod-folder-index.html @@ -4,9 +4,9 @@ - + - + @@ -17,11 +17,11 @@ - +

{{file.name}}

- +
diff --git a/src/addon/mod/folder/components/index/index.ts b/src/addon/mod/folder/components/index/index.ts index 922ec3836..805210106 100644 --- a/src/addon/mod/folder/components/index/index.ts +++ b/src/addon/mod/folder/components/index/index.ts @@ -13,8 +13,6 @@ // limitations under the License. import { Component, Input, Injector } from '@angular/core'; -import { CoreAppProvider } from '@providers/app'; -import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModFolderProvider } from '../../providers/folder'; import { AddonModFolderHelperProvider } from '../../providers/helper'; @@ -29,14 +27,16 @@ import { AddonModFolderHelperProvider } from '../../providers/helper'; templateUrl: 'addon-mod-folder-index.html', }) export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceComponent { - @Input() path: string; // For subfolders. Use the path instead of a boolean so Angular detects them as different states. + @Input() folderInstance?: any; // The mod_folder instance. + @Input() subfolder?: any; // Subfolder to show. component = AddonModFolderProvider.COMPONENT; canGetFolder: boolean; contents: any; - constructor(injector: Injector, private folderProvider: AddonModFolderProvider, private courseProvider: CoreCourseProvider, - private appProvider: CoreAppProvider, private folderHelper: AddonModFolderHelperProvider) { + constructor(injector: Injector, + protected folderProvider: AddonModFolderProvider, + protected folderHelper: AddonModFolderHelperProvider) { super(injector); } @@ -48,9 +48,9 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo this.canGetFolder = this.folderProvider.isGetFolderWSAvailable(); - if (this.path) { + if (this.subfolder) { // Subfolder. Use module param. - this.showModuleData(this.module, this.module.contents); + this.showModuleData(this.subfolder.contents); this.loaded = true; this.refreshIcon = 'refresh'; } else { @@ -77,15 +77,14 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo } /** - * Convenience function to set scope data using module. - * @param module Module to show. + * Convenience function to set data to display. + * + * @param folderContents Contents to show. */ - protected showModuleData(module: any, folderContents: any): void { - this.description = module.intro || module.description; + protected showModuleData(folderContents: any): void { + this.description = this.folderInstance ? this.folderInstance.intro : this.module.description; - this.dataRetrieved.emit(module); - - if (this.path) { + if (this.subfolder) { // Subfolder. this.contents = folderContents; } else { @@ -107,25 +106,29 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo promise = this.folderProvider.getFolder(this.courseId, this.module.id).then((folder) => { return this.courseProvider.loadModuleContents(this.module, this.courseId, undefined, false, refresh).then(() => { folderContents = this.module.contents; + this.folderInstance = folder; return folder; }); }); } else { - promise = this.courseProvider.getModule(this.module.id, this.courseId).then((folder) => { - if (!folder.contents.length && this.module.contents.length && !this.appProvider.isOnline()) { + promise = this.courseProvider.getModule(this.module.id, this.courseId).then((module) => { + if (!module.contents.length && this.module.contents.length && !this.appProvider.isOnline()) { // The contents might be empty due to a cached data. Use the old ones. - folder.contents = this.module.contents; + module.contents = this.module.contents; } - this.module = folder; - folderContents = folder.contents; + this.module = module; + folderContents = module.contents; - return folder; + return module; }); } - return promise.then((folder) => { - this.showModuleData(folder, folderContents); + return promise.then(() => { + + this.dataRetrieved.emit(this.folderInstance || this.module); + + this.showModuleData(folderContents); }).finally(() => { this.fillContextMenu(refresh); }); diff --git a/src/addon/mod/folder/pages/index/index.html b/src/addon/mod/folder/pages/index/index.html index c435f6866..27f9f1509 100644 --- a/src/addon/mod/folder/pages/index/index.html +++ b/src/addon/mod/folder/pages/index/index.html @@ -8,9 +8,9 @@ - + - + diff --git a/src/addon/mod/folder/pages/index/index.ts b/src/addon/mod/folder/pages/index/index.ts index 74c63b22e..a64d7b499 100644 --- a/src/addon/mod/folder/pages/index/index.ts +++ b/src/addon/mod/folder/pages/index/index.ts @@ -30,13 +30,15 @@ export class AddonModFolderIndexPage { title: string; module: any; courseId: number; - path: string; + folderInstance: any; + subfolder: any; constructor(navParams: NavParams) { this.module = navParams.get('module') || {}; this.courseId = navParams.get('courseId'); - this.path = navParams.get('path'); - this.title = this.module.name; + this.folderInstance = navParams.get('folderInstance'); + this.subfolder = navParams.get('subfolder'); + this.title = this.subfolder ? this.subfolder.name : this.module.name; } /** diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html index e941c514a..31e15b3e0 100644 --- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html +++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html @@ -7,7 +7,7 @@ - + @@ -43,7 +43,7 @@
- +

@@ -57,10 +57,10 @@

{{ 'core.notsent' | translate }}

-
+
- +

@@ -94,7 +94,7 @@ - + diff --git a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts index d9951cc49..17ff5888e 100644 --- a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts +++ b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts @@ -473,6 +473,8 @@ export class AddonModForumNewDiscussionPage implements OnDestroy { if (discussionIds) { // Data sent to server, delete stored files (if any). this.forumHelper.deleteNewDiscussionStoredFiles(this.forumId, discTimecreated); + + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'forum' }); } if (discussionIds && discussionIds.length < groupIds.length) { diff --git a/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html index 235c51297..0c01a1288 100644 --- a/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html @@ -14,7 +14,7 @@ - + diff --git a/src/addon/mod/glossary/pages/edit/edit.ts b/src/addon/mod/glossary/pages/edit/edit.ts index 5bc201f8c..9bc2ee645 100644 --- a/src/addon/mod/glossary/pages/edit/edit.ts +++ b/src/addon/mod/glossary/pages/edit/edit.ts @@ -246,6 +246,7 @@ export class AddonModGlossaryEditPage implements OnInit { if (entryId) { // Data sent to server, delete stored files (if any). this.glossaryHelper.deleteStoredFiles(this.glossary.id, this.entry.concept, timecreated); + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'glossary' }); } const data = { diff --git a/src/addon/mod/h5pactivity/components/components.module.ts b/src/addon/mod/h5pactivity/components/components.module.ts new file mode 100644 index 000000000..67ba634d8 --- /dev/null +++ b/src/addon/mod/h5pactivity/components/components.module.ts @@ -0,0 +1,47 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CoreCourseComponentsModule } from '@core/course/components/components.module'; +import { CoreH5PComponentsModule } from '@core/h5p/components/components.module'; +import { AddonModH5PActivityIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModH5PActivityIndexComponent, + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule, + CoreH5PComponentsModule, + ], + providers: [ + ], + exports: [ + AddonModH5PActivityIndexComponent, + ], + entryComponents: [ + AddonModH5PActivityIndexComponent, + ] +}) +export class AddonModH5PActivityComponentsModule {} diff --git a/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html new file mode 100644 index 000000000..23df4f6ad --- /dev/null +++ b/src/addon/mod/h5pactivity/components/index/addon-mod-h5pactivity-index.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + {{ 'core.hasdatatosync' | translate:{$a: moduleName} }} + + + + + {{ 'core.h5p.offlinedisabled' | translate }} {{ 'addon.mod_h5pactivity.offlinedisabledwarning' | translate }} + + + + + {{ 'addon.mod_h5pactivity.previewmode' | translate }} + + + + +

{{ stateMessage | translate }}

+
+ + + + {{ 'addon.mod_h5pactivity.downloadh5pfile' | translate }} + + + + + +

{{ progressMessage | translate }}

+ +
+
+ + +
diff --git a/src/addon/mod/h5pactivity/components/index/index.ts b/src/addon/mod/h5pactivity/components/index/index.ts new file mode 100644 index 000000000..2dc01a2d5 --- /dev/null +++ b/src/addon/mod/h5pactivity/components/index/index.ts @@ -0,0 +1,470 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, Optional, Injector } from '@angular/core'; +import { Content } from 'ionic-angular'; + +import { CoreApp } from '@providers/app'; +import { CoreEvents } from '@providers/events'; +import { CoreFilepool } from '@providers/filepool'; +import { CoreWSExternalFile } from '@providers/ws'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { CoreH5P } from '@core/h5p/providers/h5p'; +import { CoreH5PDisplayOptions } from '@core/h5p/classes/core'; +import { CoreH5PHelper } from '@core/h5p/classes/helper'; +import { CoreXAPI } from '@core/xapi/providers/xapi'; +import { CoreXAPIOffline } from '@core/xapi/providers/offline'; +import { CoreConstants } from '@core/constants'; +import { CoreSite } from '@classes/site'; + +import { + AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAccessInfo +} from '../../providers/h5pactivity'; +import { AddonModH5PActivitySyncProvider, AddonModH5PActivitySync } from '../../providers/sync'; + +/** + * Component that displays an H5P activity entry page. + */ +@Component({ + selector: 'addon-mod-h5pactivity-index', + templateUrl: 'addon-mod-h5pactivity-index.html', +}) +export class AddonModH5PActivityIndexComponent extends CoreCourseModuleMainActivityComponent { + component = AddonModH5PActivityProvider.COMPONENT; + moduleName = 'h5pactivity'; + + h5pActivity: AddonModH5PActivityData; // The H5P activity object. + accessInfo: AddonModH5PActivityAccessInfo; // Info about the user capabilities. + deployedFile: CoreWSExternalFile; // The H5P deployed file. + + stateMessage: string; // Message about the file state. + downloading: boolean; // Whether the H5P file is being downloaded. + needsDownload: boolean; // Whether the file needs to be downloaded. + percentage: string; // Download/unzip percentage. + progressMessage: string; // Message about download/unzip. + playing: boolean; // Whether the package is being played. + displayOptions: CoreH5PDisplayOptions; // Display options for the package. + onlinePlayerUrl: string; // URL to play the package in online. + fileUrl: string; // The fileUrl to use to play the package. + state: string; // State of the file. + siteCanDownload: boolean; + trackComponent: string; // Component for tracking. + hasOffline: boolean; + isOpeningPage: boolean; + + protected fetchContentDefaultError = 'addon.mod_h5pactivity.errorgetactivity'; + protected syncEventName = AddonModH5PActivitySyncProvider.AUTO_SYNCED; + protected site: CoreSite; + protected observer; + protected messageListenerFunction: (event: MessageEvent) => Promise; + + constructor(injector: Injector, + @Optional() protected content: Content) { + super(injector, content); + + this.site = this.sitesProvider.getCurrentSite(); + this.siteCanDownload = this.site.canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite(); + + // Listen for messages from the iframe. + this.messageListenerFunction = this.onIframeMessage.bind(this); + window.addEventListener('message', this.messageListenerFunction); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + super.ngOnInit(); + + this.loadContent(); + } + + /** + * Check the completion. + */ + protected checkCompletion(): void { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completiondata); + } + + /** + * Get the activity data. + * + * @param refresh If it's refreshing content. + * @param sync If it should try to sync. + * @param showErrors If show errors to the user of hide them. + * @return Promise resolved when done. + */ + protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise { + try { + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(this.courseId, this.module.id, false, this.siteId); + + this.dataRetrieved.emit(this.h5pActivity); + this.description = this.h5pActivity.intro; + this.displayOptions = CoreH5PHelper.decodeDisplayOptions(this.h5pActivity.displayoptions); + + if (sync) { + await this.syncActivity(showErrors); + } + + await Promise.all([ + this.checkHasOffline(), + this.fetchAccessInfo(), + this.fetchDeployedFileData(), + ]); + + this.trackComponent = this.accessInfo.cansubmit ? AddonModH5PActivityProvider.TRACK_COMPONENT : ''; + + if (this.h5pActivity.package && this.h5pActivity.package[0]) { + // The online player should use the original file, not the trusted one. + this.onlinePlayerUrl = CoreH5P.instance.h5pPlayer.calculateOnlinePlayerUrl( + this.site.getURL(), this.h5pActivity.package[0].fileurl, this.displayOptions, this.trackComponent); + } + + if (!this.siteCanDownload || this.state == CoreConstants.DOWNLOADED) { + // Cannot download the file or already downloaded, play the package directly. + this.play(); + + } else if ((this.state == CoreConstants.NOT_DOWNLOADED || this.state == CoreConstants.OUTDATED) && + CoreFilepool.instance.shouldDownload(this.deployedFile.filesize) && CoreApp.instance.isOnline()) { + // Package is small, download it automatically. Don't block this function for this. + this.downloadAutomatically(); + } + } finally { + this.fillContextMenu(refresh); + } + } + + /** + * Fetch the access info and store it in the right variables. + * + * @return Promise resolved when done. + */ + protected async checkHasOffline(): Promise { + this.hasOffline = await CoreXAPIOffline.instance.contextHasStatements(this.h5pActivity.context, this.siteId); + } + + /** + * Fetch the access info and store it in the right variables. + * + * @return Promise resolved when done. + */ + protected async fetchAccessInfo(): Promise { + this.accessInfo = await AddonModH5PActivity.instance.getAccessInformation(this.h5pActivity.id, false, this.siteId); + } + + /** + * Fetch the deployed file data if needed and store it in the right variables. + * + * @return Promise resolved when done. + */ + protected async fetchDeployedFileData(): Promise { + if (!this.siteCanDownload) { + // Cannot download the file, no need to fetch the file data. + return; + } + + this.deployedFile = await AddonModH5PActivity.instance.getDeployedFile(this.h5pActivity, { + displayOptions: this.displayOptions, + siteId: this.siteId, + }); + + this.fileUrl = this.deployedFile.fileurl; + + // Listen for changes in the state. + const eventName = await CoreFilepool.instance.getFileEventNameByUrl(this.siteId, this.deployedFile.fileurl); + + if (!this.observer) { + this.observer = CoreEvents.instance.on(eventName, () => { + this.calculateFileState(); + }); + } + + await this.calculateFileState(); + } + + /** + * Calculate the state of the deployed file. + * + * @return Promise resolved when done. + */ + protected async calculateFileState(): Promise { + this.state = await CoreFilepool.instance.getFileStateByUrl(this.siteId, this.deployedFile.fileurl, + this.deployedFile.timemodified); + + this.showFileState(); + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected invalidateContent(): Promise { + return AddonModH5PActivity.instance.invalidateActivityData(this.courseId); + } + + /** + * Displays some data based on the state of the main file. + */ + protected showFileState(): void { + + if (this.state == CoreConstants.OUTDATED) { + this.stateMessage = 'addon.mod_h5pactivity.filestateoutdated'; + this.needsDownload = true; + } else if (this.state == CoreConstants.NOT_DOWNLOADED) { + this.stateMessage = 'addon.mod_h5pactivity.filestatenotdownloaded'; + this.needsDownload = true; + } else if (this.state == CoreConstants.DOWNLOADING) { + this.stateMessage = ''; + + if (!this.downloading) { + // It's being downloaded right now but the view isn't tracking it. "Restore" the download. + this.downloadDeployedFile().then(() => { + this.play(); + }); + } + } else { + this.stateMessage = ''; + this.needsDownload = false; + } + } + + /** + * Download the file and play it. + * + * @param e Click event. + * @return Promise resolved when done. + */ + async downloadAndPlay(e: MouseEvent): Promise { + e && e.preventDefault(); + e && e.stopPropagation(); + + if (!CoreApp.instance.isOnline()) { + CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); + + return; + } + + try { + // Confirm the download if needed. + await CoreDomUtils.instance.confirmDownloadSize({ size: this.deployedFile.filesize, total: true }); + + await this.downloadDeployedFile(); + + if (!this.isDestroyed) { + this.play(); + } + + } catch (error) { + if (CoreDomUtils.instance.isCanceledError(error) || this.isDestroyed) { + // User cancelled or view destroyed, stop. + return; + } + + CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true); + } + } + + /** + * Download the file automatically. + * + * @return Promise resolved when done. + */ + protected async downloadAutomatically(): Promise { + try { + await this.downloadDeployedFile(); + + if (!this.isDestroyed) { + this.play(); + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true); + } + } + + /** + * Download athe H5P deployed file or restores an ongoing download. + * + * @return Promise resolved when done. + */ + protected async downloadDeployedFile(): Promise { + this.downloading = true; + this.progressMessage = 'core.downloading'; + + try { + await CoreFilepool.instance.downloadUrl(this.siteId, this.deployedFile.fileurl, false, this.component, this.componentId, + this.deployedFile.timemodified, (data) => { + + if (!data) { + return; + } + + if (data.message) { + // Show a message. + this.progressMessage = data.message; + this.percentage = undefined; + } else if (typeof data.loaded != 'undefined') { + if (this.progressMessage == 'core.downloading') { + // Downloading package. + this.percentage = (Number(data.loaded / this.deployedFile.filesize) * 100).toFixed(1); + } else if (typeof data.total != 'undefined') { + // Unzipping package. + this.percentage = (Number(data.loaded / data.total) * 100).toFixed(1); + } else { + this.percentage = undefined; + } + } else { + this.percentage = undefined; + } + }); + + } finally { + this.progressMessage = undefined; + this.percentage = undefined; + this.downloading = false; + } + } + + /** + * Play the package. + */ + play(): void { + this.playing = true; + + // Mark the activity as viewed. + AddonModH5PActivity.instance.logView(this.h5pActivity.id, this.h5pActivity.name, this.siteId); + } + + /** + * Go to view user events. + */ + async viewMyAttempts(): Promise { + this.isOpeningPage = true; + + try { + await this.navCtrl.push('AddonModH5PActivityUserAttemptsPage', { + courseId: this.courseId, + h5pActivityId: this.h5pActivity.id, + }); + } finally { + this.isOpeningPage = false; + } + } + + /** + * Treat an iframe message event. + * + * @param event Event. + * @return Promise resolved when done. + */ + protected async onIframeMessage(event: MessageEvent): Promise { + if (!event.data || !CoreXAPI.instance.canPostStatementsInSite(this.site) || !this.isCurrentXAPIPost(event.data)) { + return; + } + + try { + const options = { + offline: this.hasOffline, + courseId: this.courseId, + extra: this.h5pActivity.name, + siteId: this.site.getId(), + }; + + const sent = await CoreXAPI.instance.postStatements(this.h5pActivity.context, event.data.component, + JSON.stringify(event.data.statements), options); + + this.hasOffline = !sent; + + if (sent) { + try { + // Invalidate attempts. + await AddonModH5PActivity.instance.invalidateUserAttempts(this.h5pActivity.id, undefined, this.siteId); + } catch (error) { + // Ignore errors. + } + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error sending tracking data.'); + } + } + + /** + * Check if an event is an XAPI post statement of the current activity. + * + * @param data Event data. + * @return Whether it's an XAPI post statement of the current activity. + */ + protected isCurrentXAPIPost(data: any): boolean { + if (data.environment != 'moodleapp' || data.context != 'h5p' || data.action != 'xapi_post_statement' || !data.statements) { + return false; + } + + // Check the event belongs to this activity. + const trackingUrl = data.statements[0] && data.statements[0].object && data.statements[0].object.id; + if (!trackingUrl) { + return false; + } + + if (!this.site.containsUrl(trackingUrl)) { + // The event belongs to another site, weird scenario. Maybe some JS running in background. + return false; + } + + const match = trackingUrl.match(/xapi\/activity\/(\d+)/); + + return match && match[1] == this.h5pActivity.context; + } + + /** + * Performs the sync of the activity. + * + * @return Promise resolved when done. + */ + protected sync(): Promise { + return AddonModH5PActivitySync.instance.syncActivity(this.h5pActivity.context, this.site.getId()); + } + + /** + * An autosync event has been received. + * + * @param syncEventData Data receiven on sync observer. + */ + protected autoSyncEventReceived(syncEventData: any): void { + this.checkHasOffline(); + } + + /** + * Go to blog posts. + * + * @param event Event. + */ + async gotoBlog(event: any): Promise { + this.isOpeningPage = true; + + try { + await super.gotoBlog(event); + } finally { + this.isOpeningPage = false; + } + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.observer && this.observer.off(); + window.removeEventListener('message', this.messageListenerFunction); + } +} diff --git a/src/addon/mod/h5pactivity/h5pactivity.module.ts b/src/addon/mod/h5pactivity/h5pactivity.module.ts new file mode 100644 index 000000000..85cce71a2 --- /dev/null +++ b/src/addon/mod/h5pactivity/h5pactivity.module.ts @@ -0,0 +1,70 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; + +import { CoreCronDelegate } from '@providers/cron'; +import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate'; +import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; + +import { AddonModH5PActivityComponentsModule } from './components/components.module'; +import { AddonModH5PActivityModuleHandler } from './providers/module-handler'; +import { AddonModH5PActivityProvider } from './providers/h5pactivity'; +import { AddonModH5PActivitySyncProvider } from './providers/sync'; +import { AddonModH5PActivityPrefetchHandler } from './providers/prefetch-handler'; +import { AddonModH5PActivityIndexLinkHandler } from './providers/index-link-handler'; +import { AddonModH5PActivityReportLinkHandler } from './providers/report-link-handler'; +import { AddonModH5PActivitySyncCronHandler } from './providers/sync-cron-handler'; + +// List of providers (without handlers). +export const ADDON_MOD_H5P_ACTIVITY_PROVIDERS: any[] = [ + AddonModH5PActivityProvider, + AddonModH5PActivitySyncProvider, +]; + +@NgModule({ + declarations: [ + ], + imports: [ + AddonModH5PActivityComponentsModule + ], + providers: [ + AddonModH5PActivityProvider, + AddonModH5PActivitySyncProvider, + AddonModH5PActivityModuleHandler, + AddonModH5PActivityPrefetchHandler, + AddonModH5PActivityIndexLinkHandler, + AddonModH5PActivityReportLinkHandler, + AddonModH5PActivitySyncCronHandler, + ] +}) +export class AddonModH5PActivityModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, + moduleHandler: AddonModH5PActivityModuleHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, + prefetchHandler: AddonModH5PActivityPrefetchHandler, + linksDelegate: CoreContentLinksDelegate, + indexHandler: AddonModH5PActivityIndexLinkHandler, + reportLinkHandler: AddonModH5PActivityReportLinkHandler, + cronDelegate: CoreCronDelegate, + syncHandler: AddonModH5PActivitySyncCronHandler) { + + moduleDelegate.registerHandler(moduleHandler); + prefetchDelegate.registerHandler(prefetchHandler); + linksDelegate.registerHandler(indexHandler); + linksDelegate.registerHandler(reportLinkHandler); + cronDelegate.register(syncHandler); + } +} diff --git a/src/addon/mod/h5pactivity/lang/en.json b/src/addon/mod/h5pactivity/lang/en.json new file mode 100644 index 000000000..f48d3c44f --- /dev/null +++ b/src/addon/mod/h5pactivity/lang/en.json @@ -0,0 +1,36 @@ +{ + "all_attempts": "All user attempts", + "answer_checked": "Answer checked", + "answer_correct": "Your answer is correct", + "answer_fail": "Incorrect answer", + "answer_incorrect": "Your answer is incorrect", + "answer_pass": "Correct answer", + "attempt": "Attempt", + "attempt_completion_no": "This attempt is not marked as completed", + "attempt_completion_yes": "This attempt is completed", + "attempts_none": "This user has no attempts to display.", + "attempt_success_fail": "Fail", + "attempt_success_pass": "Pass", + "attempt_success_unknown": "Not reported", + "completion": "Completion", + "downloadh5pfile": "Download H5P file", + "duration": "Duration", + "errorgetactivity": "Error getting H5P activity data.", + "filestatenotdownloaded": "The H5P package is not downloaded. You need to download it to be able to use it.", + "filestateoutdated": "The H5P package has been modified since the last download. You need to download it again to be able to use it.", + "maxscore": "Max score", + "modulenameplural": "H5P", + "myattempts": "My attempts", + "no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.", + "offlinedisabledwarning": "You need to be online to view the H5P package.", + "outcome": "Outcome", + "previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.", + "result_fill-in": "Fill-in text", + "result_other": "Unkown interaction type", + "review_my_attempts": "View my attempts", + "score": "Score", + "score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}", + "startdate": "Start date", + "totalscore": "Total score", + "viewattempt": "View attempt {{$a}}" +} \ No newline at end of file diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.html b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.html new file mode 100644 index 000000000..4ff03aa34 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + +

{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}: {{user.fullname}}

+
+ + +

{{ 'addon.mod_h5pactivity.attempt' | translate }} #{{attempt.attempt}}

+
+ + + + + +

{{ 'addon.mod_h5pactivity.startdate' | translate }}

+

{{ attempt.timecreated | coreFormatDate:'strftimedatetime' }}

+
+ +

{{ 'addon.mod_h5pactivity.completion' | translate }}

+

+ + {{ 'addon.mod_h5pactivity.attempt_completion_yes' | translate }} +

+

+ + {{ 'addon.mod_h5pactivity.attempt_completion_no' | translate }} +

+
+ +

{{ 'addon.mod_h5pactivity.duration' | translate }}

+

{{ attempt.durationReadable }}

+
+ +

{{ 'addon.mod_h5pactivity.outcome' | translate }}

+

+ + {{ 'addon.mod_h5pactivity.attempt_success_pass' | translate }} +

+

+ + {{ 'addon.mod_h5pactivity.attempt_success_fail' | translate }} +

+

+ {{ 'addon.mod_h5pactivity.attempt_success_unknown' | translate }} +

+
+ +

{{ 'addon.mod_h5pactivity.totalscore' | translate }}

+

{{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: attempt} }}

+
+
+
+ + + + + + + + + + + + + + + + {{ result.optionslabel }} + {{ result.correctlabel }} + {{ result.answerlabel }} + + + + + + + + + + + + + + + + + + + + + + +

{{ 'addon.mod_h5pactivity.score' | translate }}: {{ 'addon.mod_h5pactivity.score_out_of' | translate:{$a: result} }}

+
+
+ + + + {{ 'addon.mod_h5pactivity.no_compatible_track' | translate:{$a: result.interactiontype} }} + +
+
+
+
+
+ + + +

+ + {{ answer.answer }} +

+

+ + {{ answer.answer }} +

+

+ {{ answer.answer }} +

+

+ +

+

+ +

+

+ +

+
diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts new file mode 100644 index 000000000..a692f5b14 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonModH5PActivityAttemptResultsPage } from './attempt-results'; + +@NgModule({ + declarations: [ + AddonModH5PActivityAttemptResultsPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonModH5PActivityAttemptResultsPage), + TranslateModule.forChild(), + ], +}) +export class AddonModH5PActivityAttemptResultsPageModule {} diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.scss b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.scss new file mode 100644 index 000000000..04b50c7d4 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.scss @@ -0,0 +1,55 @@ +ion-app.app-root page-addon-mod-h5pactivity-attempt-results { + + .addon-mod_h5pactivity-attempt-result-summary { + img { + width: 16px; + height: 16px; + display: inline; + @include margin-horizontal(0, 4px); + } + + .icon { + font-size: 1.4em; + } + } + + .addon-mod_h5pactivity-result-table-header .item-inner { + font-size: 0.9em; + font-weight: bold; + + .col[text-center] { + @include padding-horizontal(0); + } + } + + .addon-mod_h5pactivity-result-table-header, .addon-mod_h5pactivity-result-table-row { + + .item-inner ion-label { + @include margin(null, 0, null, null); + } + + .item { + @include padding(null, null, null, 0); + } + + .label { + margin-top: 0; + margin-bottom: 0; + } + + .icon { + font-size: 1.2em; + } + } + + .addon-mod_h5pactivity-result-table-row.item:nth-child(even) { + background-color: $gray-lighter; + @include darkmode() { + background-color: $core-dark-item-divider-bg-color; + } + } + + .addon-mod_h5pactivity-result-score { + border-top: 1px solid black; + } +} diff --git a/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts new file mode 100644 index 000000000..96b99fc16 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/attempt-results/attempt-results.ts @@ -0,0 +1,137 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreUser } from '@core/user/providers/user'; +import { + AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData, AddonModH5PActivityAttemptResults +} from '../../providers/h5pactivity'; + +/** + * Page that displays results of an attempt. + */ +@IonicPage({ segment: 'addon-mod-h5pactivity-attempt-results' }) +@Component({ + selector: 'page-addon-mod-h5pactivity-attempt-results', + templateUrl: 'attempt-results.html', +}) +export class AddonModH5PActivityAttemptResultsPage implements OnInit { + loaded: boolean; + h5pActivity: AddonModH5PActivityData; + attempt: AddonModH5PActivityAttemptResults; + user: any; + component = AddonModH5PActivityProvider.COMPONENT; + + protected courseId: number; + protected h5pActivityId: number; + protected attemptId: number; + + constructor(navParams: NavParams) { + this.courseId = navParams.get('courseId'); + this.h5pActivityId = navParams.get('h5pActivityId'); + this.attemptId = navParams.get('attemptId'); + } + + /** + * Component being initialized. + * + * @return Promise resolved when done. + */ + async ngOnInit(): Promise { + try { + await this.fetchData(); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading attempt.'); + } finally { + this.loaded = true; + } + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + doRefresh(refresher: any): void { + this.refreshData().finally(() => { + refresher.complete(); + }); + } + + /** + * Get quiz data and attempt data. + * + * @return Promise resolved when done. + */ + protected async fetchData(): Promise { + await Promise.all([ + this.fetchActivity(), + this.fetchAttempt(), + ]); + + await this.fetchUserProfile(); + } + + /** + * Get activity data. + * + * @return Promise resolved when done. + */ + protected async fetchActivity(): Promise { + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); + } + + /** + * Get attempts. + * + * @return Promise resolved when done. + */ + protected async fetchAttempt(): Promise { + this.attempt = await AddonModH5PActivity.instance.getAttemptResults(this.h5pActivityId, this.attemptId); + } + + /** + * Get user profile. + * + * @return Promise resolved when done. + */ + protected async fetchUserProfile(): Promise { + try { + this.user = await CoreUser.instance.getProfile(this.attempt.userid, this.courseId, true); + } catch (error) { + // Ignore errors. + } + } + + /** + * Refresh the data. + * + * @return Promise resolved when done. + */ + protected async refreshData(): Promise { + + try { + await Promise.all([ + AddonModH5PActivity.instance.invalidateActivityData(this.courseId), + AddonModH5PActivity.instance.invalidateAttemptResults(this.h5pActivityId, this.attemptId), + ]); + } catch (error) { + // Ignore errors. + } + + await this.fetchData(); + } +} diff --git a/src/addon/mod/h5pactivity/pages/index/index.html b/src/addon/mod/h5pactivity/pages/index/index.html new file mode 100644 index 000000000..420138c97 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/h5pactivity/pages/index/index.module.ts b/src/addon/mod/h5pactivity/pages/index/index.module.ts new file mode 100644 index 000000000..b75f46b1c --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/index/index.module.ts @@ -0,0 +1,33 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { AddonModH5PActivityComponentsModule } from '../../components/components.module'; +import { AddonModH5PActivityIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModH5PActivityIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModH5PActivityComponentsModule, + IonicPageModule.forChild(AddonModH5PActivityIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModH5PActivityIndexPageModule {} diff --git a/src/addon/mod/h5pactivity/pages/index/index.ts b/src/addon/mod/h5pactivity/pages/index/index.ts new file mode 100644 index 000000000..e3bc7e304 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/index/index.ts @@ -0,0 +1,65 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, ViewChild } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { AddonModH5PActivityIndexComponent } from '../../components/index/index'; +import { AddonModH5PActivityData } from '../../providers/h5pactivity'; + +import { Translate } from '@singletons/core.singletons'; + +/** + * Page that displays an H5P activity. + */ +@IonicPage({ segment: 'addon-mod-h5pactivity-index' }) +@Component({ + selector: 'page-addon-mod-h5pactivity-index', + templateUrl: 'index.html', +}) +export class AddonModH5PActivityIndexPage { + @ViewChild(AddonModH5PActivityIndexComponent) h5pComponent: AddonModH5PActivityIndexComponent; + + title: string; + module: any; + courseId: number; + + constructor(navParams: NavParams) { + this.module = navParams.get('module') || {}; + this.courseId = navParams.get('courseId'); + this.title = this.module.name; + } + + /** + * Update some data based on the H5P activity instance. + * + * @param h5p H5P activity instance. + */ + updateData(h5p: AddonModH5PActivityData): void { + this.title = h5p.name || this.title; + } + + /** + * Check if we can leave the page or not. + * + * @return Resolved if we can leave it, rejected if not. + */ + ionViewCanLeave(): Promise { + if (!this.h5pComponent.playing || this.h5pComponent.isOpeningPage) { + return; + } + + return CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmleaveunknownchanges')); + } +} diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.html b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.html new file mode 100644 index 000000000..a7dc50709 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.html @@ -0,0 +1,81 @@ + + + + + + + + + + + + + +

{{ user.fullname }}

+
+ + +

{{ 'addon.mod_h5pactivity.myattempts' | translate }}

+
+ + + + + +

{{ attemptsData.scored.title }}

+
+ +
+ + + + +

{{ 'addon.mod_h5pactivity.all_attempts' | translate }}

+
+ +
+
+ + + + +
+
+ + + + + + + # + {{ 'core.date' | translate }} + {{ 'addon.mod_h5pactivity.score' | translate }} + {{ 'addon.mod_h5pactivity.maxscore' | translate }} + {{ 'addon.mod_h5pactivity.duration' | translate }} + {{ 'addon.mod_h5pactivity.completion' | translate }} + {{ 'core.success' | translate }} + + + + + + + {{ attempt.attempt }} + {{ attempt.timemodified | coreFormatDate:'strftimedatetimeshort' }} + + {{ attempt.rawscore }} / {{ attempt.maxscore }} + + {{ attempt.maxscore }} + {{ attempt.durationReadable }} + + + + + + + + + + + + \ No newline at end of file diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts new file mode 100644 index 000000000..8532f1bb4 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.module.ts @@ -0,0 +1,35 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonModH5PActivityUserAttemptsPage } from './user-attempts'; + +@NgModule({ + declarations: [ + AddonModH5PActivityUserAttemptsPage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonModH5PActivityUserAttemptsPage), + TranslateModule.forChild(), + ], +}) +export class AddonModH5PActivityUserAttemptsPageModule {} diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.scss b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.scss new file mode 100644 index 000000000..f6a9381b0 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.scss @@ -0,0 +1,37 @@ +ion-app.app-root page-addon-mod-h5pactivity-user-attempts { + + .item.addon-mod_h5pactivity-table-header[detail-push] .item-inner { + background-image: none; + } + + .item.addon-mod_h5pactivity-table-header .item-inner { + font-size: 0.9em; + font-weight: bold; + + .col[text-center] { + @include padding-horizontal(0); + } + } + + .addon-mod_h5pactivity-table-header, .addon-mod_h5pactivity-table-row { + + .item-inner ion-label { + @include margin(null, 0, null, null); + } + + .item { + @include padding(null, null, null, 0); + } + + .label { + margin-top: 0; + margin-bottom: 0; + } + } + + .addon-mod_h5pactivity-table-row { + .addon-mod_h5pactivity-table-success-col { + font-size: 1.4em; + } + } +} diff --git a/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts new file mode 100644 index 000000000..cc8499405 --- /dev/null +++ b/src/addon/mod/h5pactivity/pages/user-attempts/user-attempts.ts @@ -0,0 +1,138 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { IonicPage, NavParams } from 'ionic-angular'; +import { CoreSites } from '@providers/sites'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreUser } from '@core/user/providers/user'; +import { + AddonModH5PActivity, AddonModH5PActivityData, AddonModH5PActivityUserAttempts +} from '../../providers/h5pactivity'; + +/** + * Page that displays user attempts of a certain user. + */ +@IonicPage({ segment: 'addon-mod-h5pactivity-user-attempts' }) +@Component({ + selector: 'page-addon-mod-h5pactivity-user-attempts', + templateUrl: 'user-attempts.html', +}) +export class AddonModH5PActivityUserAttemptsPage implements OnInit { + loaded: boolean; + courseId: number; + h5pActivityId: number; + h5pActivity: AddonModH5PActivityData; + attemptsData: AddonModH5PActivityUserAttempts; + user: any; + isCurrentUser: boolean; + + protected userId: number; + + constructor(navParams: NavParams) { + this.courseId = navParams.get('courseId'); + this.h5pActivityId = navParams.get('h5pActivityId'); + this.userId = navParams.get('userId') || CoreSites.instance.getCurrentSiteUserId(); + this.isCurrentUser = this.userId == CoreSites.instance.getCurrentSiteUserId(); + } + + /** + * Component being initialized. + * + * @return Promise resolved when done. + */ + async ngOnInit(): Promise { + try { + await this.fetchData(); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading attempts.'); + } finally { + this.loaded = true; + } + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + */ + doRefresh(refresher: any): void { + this.refreshData().finally(() => { + refresher.complete(); + }); + } + + /** + * Get quiz data and attempt data. + * + * @return Promise resolved when done. + */ + protected async fetchData(): Promise { + await Promise.all([ + this.fetchActivity(), + this.fetchAttempts(), + this.fetchUserProfile(), + ]); + } + + /** + * Get activity data. + * + * @return Promise resolved when done. + */ + protected async fetchActivity(): Promise { + this.h5pActivity = await AddonModH5PActivity.instance.getH5PActivityById(this.courseId, this.h5pActivityId); + } + + /** + * Get attempts. + * + * @return Promise resolved when done. + */ + protected async fetchAttempts(): Promise { + this.attemptsData = await AddonModH5PActivity.instance.getUserAttempts(this.h5pActivityId, { userId: this.userId }); + } + + /** + * Get user profile. + * + * @return Promise resolved when done. + */ + protected async fetchUserProfile(): Promise { + try { + this.user = await CoreUser.instance.getProfile(this.userId, this.courseId, true); + } catch (error) { + // Ignore errors. + } + } + + /** + * Refresh the data. + * + * @return Promise resolved when done. + */ + protected async refreshData(): Promise { + + try { + await Promise.all([ + AddonModH5PActivity.instance.invalidateActivityData(this.courseId), + AddonModH5PActivity.instance.invalidateUserAttempts(this.h5pActivityId, this.userId), + ]); + } catch (error) { + // Ignore errors. + } + + await this.fetchData(); + } +} diff --git a/src/addon/mod/h5pactivity/providers/h5pactivity.ts b/src/addon/mod/h5pactivity/providers/h5pactivity.ts new file mode 100644 index 000000000..5e1fabc42 --- /dev/null +++ b/src/addon/mod/h5pactivity/providers/h5pactivity.ts @@ -0,0 +1,802 @@ +// (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 '@providers/sites'; +import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { CoreTimeUtils } from '@providers/utils/time'; +import { CoreUtils } from '@providers/utils/utils'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourseLogHelper } from '@core/course/providers/log-helper'; +import { CoreH5P } from '@core/h5p/providers/h5p'; +import { CoreH5PDisplayOptions } from '@core/h5p/classes/core'; + +import { makeSingleton, Translate } from '@singletons/core.singletons'; + +/** + * Service that provides some features for H5P activity. + */ +@Injectable() +export class AddonModH5PActivityProvider { + static COMPONENT = 'mmaModH5PActivity'; + static TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking. + + protected ROOT_CACHE_KEY = 'mmaModH5PActivity:'; + + /** + * Format an attempt's data. + * + * @param attempt Attempt to format. + */ + protected formatAttempt(attempt: AddonModH5PActivityWSAttempt): AddonModH5PActivityAttempt { + const formattedAttempt: AddonModH5PActivityAttempt = attempt; + + formattedAttempt.timecreated = attempt.timecreated * 1000; // Convert to milliseconds. + formattedAttempt.timemodified = attempt.timemodified * 1000; // Convert to milliseconds. + formattedAttempt.success = typeof formattedAttempt.success == 'undefined' ? null : formattedAttempt.success; + + if (!attempt.duration) { + formattedAttempt.durationReadable = '-'; + formattedAttempt.durationCompact = '-'; + } else { + formattedAttempt.durationReadable = CoreTimeUtils.instance.formatTime(attempt.duration); + formattedAttempt.durationCompact = CoreTimeUtils.instance.formatDurationShort(attempt.duration); + } + + return formattedAttempt; + } + + /** + * Format attempt data and results. + * + * @param attempt Attempt and results to format. + */ + protected formatAttemptResults(attempt: AddonModH5PActivityWSAttemptResults): AddonModH5PActivityAttemptResults { + const formattedAttempt: AddonModH5PActivityAttemptResults = this.formatAttempt(attempt); + + formattedAttempt.results = formattedAttempt.results.map((result) => { + return this.formatResult(result); + }); + + return formattedAttempt; + } + + /** + * Format the attempts of a user. + * + * @param data Data to format. + * @return Formatted data. + */ + protected formatUserAttempts(data: AddonModH5PActivityWSUserAttempts): AddonModH5PActivityUserAttempts { + const formatted: AddonModH5PActivityUserAttempts = data; + + formatted.attempts = formatted.attempts.map((attempt) => { + return this.formatAttempt(attempt); + }); + + if (formatted.scored) { + + formatted.scored.attempts = formatted.scored.attempts.map((attempt) => { + return this.formatAttempt(attempt); + }); + } + + return formatted; + } + + /** + * Format an attempt's result. + * + * @param result Result to format. + */ + protected formatResult(result: AddonModH5PActivityWSResult): AddonModH5PActivityWSResult { + result.timecreated = result.timecreated * 1000; // Convert to milliseconds. + + return result; + } + + /** + * Get cache key for access information WS calls. + * + * @param id H5P activity ID. + * @return Cache key. + */ + protected getAccessInformationCacheKey(id: number): string { + return this.ROOT_CACHE_KEY + 'accessInfo:' + id; + } + + /** + * Get access information for a given H5P activity. + * + * @param id H5P activity ID. + * @param forceCache True to always get the value from cache. false otherwise. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the data. + */ + async getAccessInformation(id: number, forceCache?: boolean, siteId?: string): Promise { + + const site = await CoreSites.instance.getSite(siteId); + + const params = { + h5pactivityid: id, + }; + const preSets = { + cacheKey: this.getAccessInformationCacheKey(id), + omitExpires: forceCache, + }; + + return site.read('mod_h5pactivity_get_h5pactivity_access_information', params, preSets); + } + + /** + * Get attempt results for all user attempts. + * + * @param id Activity ID. + * @param options Other options. + * @return Promise resolved with the results of the attempt. + */ + async getAllAttemptsResults(id: number, options?: AddonModH5PActivityGetAttemptResultsOptions) + : Promise { + + const userAttempts = await AddonModH5PActivity.instance.getUserAttempts(id, options); + + const attemptIds = userAttempts.attempts.map((attempt) => { + return attempt.id; + }); + + if (attemptIds.length) { + // Get all the attempts with a single call. + return AddonModH5PActivity.instance.getAttemptsResults(id, attemptIds, options); + } else { + // No attempts. + return { + activityid: id, + attempts: [], + warnings: [], + }; + } + } + + /** + * Get cache key for results WS calls. + * + * @param id Instance ID. + * @param attemptsIds Attempts IDs. + * @return Cache key. + */ + protected getAttemptResultsCacheKey(id: number, attemptsIds: number[]): string { + return this.getAttemptResultsCommonCacheKey(id) + ':' + JSON.stringify(attemptsIds); + } + + /** + * Get common cache key for results WS calls. + * + * @param id Instance ID. + * @return Cache key. + */ + protected getAttemptResultsCommonCacheKey(id: number): string { + return this.ROOT_CACHE_KEY + 'results:' + id; + } + + /** + * Get attempt results. + * + * @param id Activity ID. + * @param attemptId Attempt ID. + * @param options Other options. + * @return Promise resolved with the results of the attempt. + */ + async getAttemptResults(id: number, attemptId: number, options?: AddonModH5PActivityGetAttemptResultsOptions) + : Promise { + + options = options || {}; + + const site = await CoreSites.instance.getSite(options.siteId); + + const params = { + h5pactivityid: id, + attemptids: [attemptId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (options.forceCache) { + preSets.omitExpires = true; + } else if (options.ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + try { + const response: AddonModH5PActivityGetResultsResult = await site.read('mod_h5pactivity_get_results', params, preSets); + + if (response.warnings[0]) { + throw response.warnings[0]; // Cannot view attempt. + } + + return this.formatAttemptResults(response.attempts[0]); + } catch (error) { + if (CoreUtils.instance.isWebServiceError(error)) { + throw error; + } + + // Check if the full list of results is cached. If so, get the results from there. + options.forceCache = true; + + const attemptsResults = await AddonModH5PActivity.instance.getAllAttemptsResults(id, options); + + const attempt = attemptsResults.attempts.find((attempt) => { + return attempt.id == attemptId; + }); + + if (!attempt) { + throw error; + } + + return attempt; + } + } + + /** + * Get attempts results. + * + * @param id Activity ID. + * @param attemptsIds Attempts IDs. + * @param options Other options. + * @return Promise resolved with all the attempts. + */ + async getAttemptsResults(id: number, attemptsIds: number[], options?: AddonModH5PActivityGetAttemptResultsOptions) + : Promise { + + options = options || {}; + + const site = await CoreSites.instance.getSite(options.siteId); + + const params = { + h5pactivityid: id, + attemptids: attemptsIds, + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getAttemptResultsCommonCacheKey(id), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (options.forceCache) { + preSets.omitExpires = true; + } else if (options.ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + const response: AddonModH5PActivityGetResultsResult = await site.read('mod_h5pactivity_get_results', params, preSets); + + response.attempts = response.attempts.map((attempt) => { + return this.formatAttemptResults(attempt); + }); + + return response; + } + + /** + * Get deployed file from an H5P activity instance. + * + * @param h5pActivity Activity instance. + * @param options Options + * @return Promise resolved with the file. + */ + async getDeployedFile(h5pActivity: AddonModH5PActivityData, options?: AddonModH5PActivityGetDeployedFileOptions) + : Promise { + + if (h5pActivity.deployedfile) { + // File already deployed and still valid, use this one. + return h5pActivity.deployedfile; + } else { + if (!h5pActivity.package || !h5pActivity.package[0]) { + // Shouldn't happen. + throw 'No H5P package found.'; + } + + options = options || {}; + + // Deploy the file in the server. + return CoreH5P.instance.getTrustedH5PFile(h5pActivity.package[0].fileurl, options.displayOptions, + options.ignoreCache, options.siteId); + } + } + + /** + * Get cache key for H5P activity data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getH5PActivityDataCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'h5pactivity:' + courseId; + } + + /** + * Get an H5P activity with key=value. If more than one is found, only the first will be returned. + * + * @param courseId Course ID. + * @param key Name of the property to check. + * @param value Value to search. + * @param moduleUrl Module URL. + * @param forceCache Whether it should always return cached data. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the activity data. + */ + protected async getH5PActivityByField(courseId: number, key: string, value: any, forceCache?: boolean, siteId?: string) + : Promise { + + const site = await CoreSites.instance.getSite(siteId); + + const params = { + courseids: [courseId], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getH5PActivityDataCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + if (forceCache) { + preSets.omitExpires = true; + } + + const response: AddonModH5PActivityGetByCoursesResult = + await site.read('mod_h5pactivity_get_h5pactivities_by_courses', params, preSets); + + if (response && response.h5pactivities) { + const currentActivity = response.h5pactivities.find((h5pActivity) => { + return h5pActivity[key] == value; + }); + + if (currentActivity) { + return currentActivity; + } + } + + throw Translate.instance.instant('addon.mod_h5pactivity.errorgetactivity'); + } + + /** + * Get an H5P activity by module ID. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param forceCache Whether it should always return cached data. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the activity data. + */ + getH5PActivity(courseId: number, cmId: number, forceCache?: boolean, siteId?: string): Promise { + return this.getH5PActivityByField(courseId, 'coursemodule', cmId, forceCache, siteId); + } + + /** + * Get an H5P activity by context ID. + * + * @param courseId Course ID. + * @param contextId Context ID. + * @param forceCache Whether it should always return cached data. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the activity data. + */ + getH5PActivityByContextId(courseId: number, contextId: number, forceCache?: boolean, siteId?: string) + : Promise { + return this.getH5PActivityByField(courseId, 'context', contextId, forceCache, siteId); + } + + /** + * Get an H5P activity by instance ID. + * + * @param courseId Course ID. + * @param id Instance ID. + * @param forceCache Whether it should always return cached data. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with the activity data. + */ + getH5PActivityById(courseId: number, id: number, forceCache?: boolean, siteId?: string): Promise { + return this.getH5PActivityByField(courseId, 'id', id, forceCache, siteId); + } + + /** + * Get cache key for attemps WS calls. + * + * @param id Instance ID. + * @param userIds User IDs. + * @return Cache key. + */ + protected getUserAttemptsCacheKey(id: number, userIds: number[]): string { + return this.getUserAttemptsCommonCacheKey(id) + ':' + JSON.stringify(userIds); + } + + /** + * Get common cache key for attempts WS calls. + * + * @param id Instance ID. + * @return Cache key. + */ + protected getUserAttemptsCommonCacheKey(id: number): string { + return this.ROOT_CACHE_KEY + 'attempts:' + id; + } + + /** + * Get attempts of a certain user. + * + * @param id Activity ID. + * @param options Other options. + * @return Promise resolved with the attempts of the user. + */ + async getUserAttempts(id: number, options?: AddonModH5PActivityGetAttemptsOptions): Promise { + + options = options || {}; + + const site = await CoreSites.instance.getSite(options.siteId); + + const params = { + h5pactivityid: id, + userids: [options.userId || site.getUserId()], + }; + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getUserAttemptsCacheKey(id, params.userids), + updateFrequency: CoreSite.FREQUENCY_SOMETIMES, + }; + + if (options.forceCache) { + preSets.omitExpires = true; + } else if (options.ignoreCache) { + preSets.getFromCache = false; + preSets.emergencyCache = false; + } + + const response: AddonModH5PActivityGetAttemptsResult = await site.read('mod_h5pactivity_get_attempts', params, preSets); + + if (response.warnings[0]) { + throw response.warnings[0]; // Cannot view user attempts. + } + + return this.formatUserAttempts(response.usersattempts[0]); + } + + /** + * Invalidates access information. + * + * @param id H5P activity ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAccessInformation(id: number, siteId?: string): Promise { + + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(id)); + } + + /** + * Invalidates H5P activity data. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateActivityData(courseId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getH5PActivityDataCacheKey(courseId)); + } + + /** + * Invalidates all attempts results for H5P activity. + * + * @param id Activity ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllResults(id: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getAttemptResultsCommonCacheKey(id)); + } + + /** + * Invalidates results of a certain attempt for H5P activity. + * + * @param id Activity ID. + * @param attemptId Attempt ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAttemptResults(id: number, attemptId: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getAttemptResultsCacheKey(id, [attemptId])); + } + + /** + * Invalidates all users attempts for H5P activity. + * + * @param id Activity ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateAllUserAttempts(id: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getUserAttemptsCommonCacheKey(id)); + } + + /** + * Invalidates attempts of a certain user for H5P activity. + * + * @param id Activity ID. + * @param userId User ID. If not defined, current user in the site. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateUserAttempts(id: number, userId?: number, siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + userId = userId || site.getUserId(); + + await site.invalidateWsCacheForKey(this.getUserAttemptsCacheKey(id, [userId])); + } + + /** + * Delete launcher. + * + * @return Promise resolved when the launcher file is deleted. + */ + async isPluginEnabled(siteId?: string): Promise { + const site = await CoreSites.instance.getSite(siteId); + + return site.wsAvailable('mod_h5pactivity_get_h5pactivities_by_courses'); + } + + /** + * Report an H5P activity as being viewed. + * + * @param id H5P activity ID. + * @param name Name of the activity. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + logView(id: number, name?: string, siteId?: string): Promise { + const params = { + h5pactivityid: id, + }; + + return CoreCourseLogHelper.instance.logSingle( + 'mod_h5pactivity_view_h5pactivity', + params, + AddonModH5PActivityProvider.COMPONENT, + id, + name, + 'h5pactivity', + {}, + siteId + ); + } +} + +export class AddonModH5PActivity extends makeSingleton(AddonModH5PActivityProvider) {} + +/** + * Basic data for an H5P activity, exported by Moodle class h5pactivity_summary_exporter. + */ +export type AddonModH5PActivityData = { + id: number; // The primary key of the record. + course: number; // Course id this h5p activity is part of. + name: string; // The name of the activity module instance. + timecreated?: number; // Timestamp of when the instance was added to the course. + timemodified?: number; // Timestamp of when the instance was last modified. + intro: string; // H5P activity description. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + grade?: number; // The maximum grade for submission. + displayoptions: number; // H5P Button display options. + enabletracking: number; // Enable xAPI tracking. + grademethod: number; // Which H5P attempt is used for grading. + contenthash?: string; // Sha1 hash of file content. + coursemodule: number; // Coursemodule. + context: number; // Context ID. + introfiles: CoreWSExternalFile[]; + package: CoreWSExternalFile[]; + deployedfile?: { + filename?: string; // File name. + filepath?: string; // File path. + filesize?: number; // File size. + fileurl?: string; // Downloadable file url. + timemodified?: number; // Time modified. + mimetype?: string; // File mime type. + }; +}; + +/** + * Result of WS mod_h5pactivity_get_h5pactivities_by_courses. + */ +export type AddonModH5PActivityGetByCoursesResult = { + h5pactivities: AddonModH5PActivityData[]; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_h5pactivity_get_h5pactivity_access_information. + */ +export type AddonModH5PActivityAccessInfo = { + warnings?: CoreWSExternalWarning[]; + canview?: boolean; // Whether the user has the capability mod/h5pactivity:view allowed. + canaddinstance?: boolean; // Whether the user has the capability mod/h5pactivity:addinstance allowed. + cansubmit?: boolean; // Whether the user has the capability mod/h5pactivity:submit allowed. + canreviewattempts?: boolean; // Whether the user has the capability mod/h5pactivity:reviewattempts allowed. +}; + +/** + * Result of WS mod_h5pactivity_get_attempts. + */ +export type AddonModH5PActivityGetAttemptsResult = { + activityid: number; // Activity course module ID. + usersattempts: AddonModH5PActivityWSUserAttempts[]; // The complete users attempts list. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Result of WS mod_h5pactivity_get_results. + */ +export type AddonModH5PActivityGetResultsResult = { + activityid: number; // Activity course module ID. + attempts: AddonModH5PActivityWSAttemptResults[]; // The complete attempts list. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Attempts data for a user as returned by the WS mod_h5pactivity_get_attempts. + */ +export type AddonModH5PActivityWSUserAttempts = { + userid: number; // The user id. + attempts: AddonModH5PActivityWSAttempt[]; // The complete attempts list. + scored?: { // Attempts used to grade the activity. + title: string; // Scored attempts title. + grademethod: string; // Scored attempts title. + attempts: AddonModH5PActivityWSAttempt[]; // List of the grading attempts. + }; +}; + +/** + * Attempt data as returned by the WS mod_h5pactivity_get_attempts. + */ +export type AddonModH5PActivityWSAttempt = { + id: number; // ID of the context. + h5pactivityid: number; // ID of the H5P activity. + userid: number; // ID of the user. + timecreated: number; // Attempt creation. + timemodified: number; // Attempt modified. + attempt: number; // Attempt number. + rawscore: number; // Attempt score value. + maxscore: number; // Attempt max score. + duration: number; // Attempt duration in seconds. + completion?: number; // Attempt completion. + success?: number; // Attempt success. + scaled: number; // Attempt scaled. +}; + +/** + * Attempt and results data as returned by the WS mod_h5pactivity_get_results. + */ +export type AddonModH5PActivityWSAttemptResults = AddonModH5PActivityWSAttempt & { + results?: AddonModH5PActivityWSResult[]; // The results of the attempt. +}; + +/** + * Attempt result data as returned by the WS mod_h5pactivity_get_results. + */ +export type AddonModH5PActivityWSResult = { + id: number; // ID of the context. + attemptid: number; // ID of the H5P attempt. + subcontent: string; // Subcontent identifier. + timecreated: number; // Result creation. + interactiontype: string; // Interaction type. + description: string; // Result description. + content?: string; // Result extra content. + rawscore: number; // Result score value. + maxscore: number; // Result max score. + duration?: number; // Result duration in seconds. + completion?: number; // Result completion. + success?: number; // Result success. + optionslabel?: string; // Label used for result options. + correctlabel?: string; // Label used for correct answers. + answerlabel?: string; // Label used for user answers. + track?: boolean; // If the result has valid track information. + options?: { // The statement options. + description: string; // Option description. + id: number; // Option identifier. + correctanswer: AddonModH5PActivityWSResultAnswer; // The option correct answer. + useranswer: AddonModH5PActivityWSResultAnswer; // The option user answer. + }[]; +}; + +/** + * Result answer as returned by the WS mod_h5pactivity_get_results. + */ +export type AddonModH5PActivityWSResultAnswer = { + answer?: string; // Option text value. + correct?: boolean; // If has to be displayed as correct. + incorrect?: boolean; // If has to be displayed as incorrect. + text?: boolean; // If has to be displayed as simple text. + checked?: boolean; // If has to be displayed as a checked option. + unchecked?: boolean; // If has to be displayed as a unchecked option. + pass?: boolean; // If has to be displayed as passed. + fail?: boolean; // If has to be displayed as failed. +}; + +/** + * User attempts data with some calculated data. + */ +export type AddonModH5PActivityUserAttempts = { + userid: number; // The user id. + attempts: AddonModH5PActivityAttempt[]; // The complete attempts list. + scored?: { // Attempts used to grade the activity. + title: string; // Scored attempts title. + grademethod: string; // Scored attempts title. + attempts: AddonModH5PActivityAttempt[]; // List of the grading attempts. + }; +}; + +/** + * Attempts results with some calculated data. + */ +export type AddonModH5PActivityAttemptsResults = { + activityid: number; // Activity course module ID. + attempts: AddonModH5PActivityAttemptResults[]; // The complete attempts list. + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Attempt with some calculated data. + */ +export type AddonModH5PActivityAttempt = AddonModH5PActivityWSAttempt & { + durationReadable?: string; // Duration in a human readable format. + durationCompact?: string; // Duration in a "short" human readable format. +}; + +/** + * Attempt and results data with some calculated data. + */ +export type AddonModH5PActivityAttemptResults = AddonModH5PActivityAttempt & { + results?: AddonModH5PActivityWSResult[]; // The results of the attempt. +}; + +/** + * Options to pass to getDeployedFile function. + */ +export type AddonModH5PActivityGetDeployedFileOptions = { + displayOptions?: CoreH5PDisplayOptions; // Display options + ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options to pass to getAttemptResults function. + */ +export type AddonModH5PActivityGetAttemptResultsOptions = { + forceCache?: boolean; // Whether to force cache. If not cached, it will call the WS. + ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down. + siteId?: string; // Site ID. If not defined, current site. + userId?: number; // User ID. If not defined, user of the site. +}; + +/** + * Options to pass to getAttempts function. + */ +export type AddonModH5PActivityGetAttemptsOptions = AddonModH5PActivityGetAttemptResultsOptions; diff --git a/src/addon/mod/h5pactivity/providers/index-link-handler.ts b/src/addon/mod/h5pactivity/providers/index-link-handler.ts new file mode 100644 index 000000000..a72c66593 --- /dev/null +++ b/src/addon/mod/h5pactivity/providers/index-link-handler.ts @@ -0,0 +1,29 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; + +/** + * Handler to treat links to H5P activity index. + */ +@Injectable() +export class AddonModH5PActivityIndexLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModH5PActivityIndexLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider) { + super(courseHelper, 'AddonModH5PActivity', 'h5pactivity'); + } +} diff --git a/src/addon/mod/h5pactivity/providers/module-handler.ts b/src/addon/mod/h5pactivity/providers/module-handler.ts new file mode 100644 index 000000000..6f544a16c --- /dev/null +++ b/src/addon/mod/h5pactivity/providers/module-handler.ts @@ -0,0 +1,90 @@ +// (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 { NavController, NavOptions } from 'ionic-angular'; + +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourse } from '@core/course/providers/course'; +import { CoreConstants } from '@core/constants'; + +import { AddonModH5PActivity } from './h5pactivity'; +import { AddonModH5PActivityIndexComponent } from '../components/index/index'; + +/** + * Handler to support H5P activities. + */ +@Injectable() +export class AddonModH5PActivityModuleHandler implements CoreCourseModuleHandler { + name = 'AddonModH5PActivity'; + modName = 'h5pactivity'; + + supportedFeatures = { + [CoreConstants.FEATURE_GROUPS]: true, + [CoreConstants.FEATURE_GROUPINGS]: true, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_MODEDIT_DEFAULT_COMPLETION]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: true, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: true, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + }; + + /** + * Check if the handler is enabled on a site level. + * + * @return Whether or not the handler is enabled on a site level. + */ + isEnabled(): Promise { + return AddonModH5PActivity.instance.isPluginEnabled(); + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param module The module object. + * @param courseId The course ID. + * @param sectionId The section ID. + * @return Data to render the module. + */ + getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData { + + return { + icon: CoreCourse.instance.getModuleIconSrc(this.modName, module.modicon), + title: module.name, + class: 'addon-mod_h5pactivity-handler', + showDownloadButton: true, + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions, params?: any): void { + const pageParams = {module: module, courseId: courseId}; + if (params) { + Object.assign(pageParams, params); + } + navCtrl.push('AddonModH5PActivityIndexPage', pageParams, options); + } + }; + } + + /** + * Get the component to render the module. This is needed to support singleactivity course format. + * The component returned must implement CoreCourseModuleMainComponent. + * + * @param course The course object. + * @param module The module object. + * @return The component to use, undefined if not found. + */ + getMainComponent(course: any, module: any): any { + return AddonModH5PActivityIndexComponent; + } +} diff --git a/src/addon/mod/h5pactivity/providers/prefetch-handler.ts b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts new file mode 100644 index 000000000..85a804baf --- /dev/null +++ b/src/addon/mod/h5pactivity/providers/prefetch-handler.ts @@ -0,0 +1,191 @@ +// (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 { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreWSExternalFile } from '@providers/ws'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; +import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; +import { CoreH5PHelper } from '@core/h5p/classes/helper'; +import { CoreH5P } from '@core/h5p/providers/h5p'; +import { CoreUser } from '@core/user/providers/user'; +import { AddonModH5PActivity, AddonModH5PActivityProvider, AddonModH5PActivityData } from './h5pactivity'; + +/** + * Handler to prefetch h5p activity. + */ +@Injectable() +export class AddonModH5PActivityPrefetchHandler extends CoreCourseActivityPrefetchHandlerBase { + name = 'AddonModH5PActivity'; + modName = 'h5pactivity'; + component = AddonModH5PActivityProvider.COMPONENT; + updatesNames = /^configuration$|^.*files$|^tracks$|^usertracks$/; + + constructor(translate: TranslateService, + appProvider: CoreAppProvider, + utils: CoreUtilsProvider, + courseProvider: CoreCourseProvider, + filepoolProvider: CoreFilepoolProvider, + sitesProvider: CoreSitesProvider, + domUtils: CoreDomUtilsProvider, + filterHelper: CoreFilterHelperProvider, + pluginFileDelegate: CorePluginFileDelegate) { + + super(translate, appProvider, utils, courseProvider, filepoolProvider, sitesProvider, domUtils, filterHelper, + pluginFileDelegate); + } + + /** + * Get list of files. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @param single True if we're downloading a single module, false if we're downloading a whole section. + * @return Promise resolved with the list of files. + */ + async getFiles(module: any, courseId: number, single?: boolean): Promise { + + const h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(courseId, module.id); + + const displayOptions = CoreH5PHelper.decodeDisplayOptions(h5pActivity.displayoptions); + + const deployedFile = await AddonModH5PActivity.instance.getDeployedFile(h5pActivity, { + displayOptions: displayOptions, + }); + + return [deployedFile].concat(this.getIntroFilesFromInstance(module, h5pActivity)); + } + + /** + * Invalidate WS calls needed to determine module status (usually, to check if module is downloadable). + * It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @return Promise resolved when invalidated. + */ + async invalidateModule(module: any, courseId: number): Promise { + // No need to invalidate anything. + } + + /** + * Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @return Whether the module can be downloaded. The promise should never be rejected. + */ + isDownloadable(module: any, courseId: number): boolean | Promise { + return this.sitesProvider.getCurrentSite().canDownloadFiles() && !CoreH5P.instance.isOfflineDisabledInSite(); + } + + /** + * Whether or not the handler is enabled on a site level. + * + * @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled. + */ + isEnabled(): boolean | Promise { + return AddonModH5PActivity.instance.isPluginEnabled(); + } + + /** + * Prefetch a module. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @param single True if we're downloading a single module, false if we're downloading a whole section. + * @param dirPath Path of the directory where to store all the content files. + * @return Promise resolved when done. + */ + prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise { + return this.prefetchPackage(module, courseId, single, this.prefetchActivity.bind(this)); + } + + /** + * Prefetch an H5P activity. + * + * @param module Module. + * @param courseId Course ID the module belongs to. + * @param single True if we're downloading a single module, false if we're downloading a whole section. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchActivity(module: any, courseId: number, single: boolean, siteId: string): Promise { + + const h5pActivity = await AddonModH5PActivity.instance.getH5PActivity(courseId, module.id, true, siteId); + + const introFiles = this.getIntroFilesFromInstance(module, h5pActivity); + + await Promise.all([ + this.prefetchWSData(h5pActivity, siteId), + this.filepoolProvider.addFilesToQueue(siteId, introFiles, AddonModH5PActivityProvider.COMPONENT, module.id), + this.prefetchMainFile(module, h5pActivity, siteId), + ]); + } + + /** + * Prefetch the deployed file of the activity. + * + * @param module Module. + * @param h5pActivity Activity instance. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchMainFile(module: any, h5pActivity: AddonModH5PActivityData, siteId: string): Promise { + + const displayOptions = CoreH5PHelper.decodeDisplayOptions(h5pActivity.displayoptions); + + const deployedFile = await AddonModH5PActivity.instance.getDeployedFile(h5pActivity, { + displayOptions: displayOptions, + ignoreCache: true, + siteId: siteId, + }); + + await this.filepoolProvider.addFilesToQueue(siteId, [deployedFile], AddonModH5PActivityProvider.COMPONENT, module.id); + } + + /** + * Prefetch all the WebService data. + * + * @param h5pActivity Activity instance. + * @param siteId Site ID. + * @return Promise resolved when done. + */ + protected async prefetchWSData(h5pActivity: AddonModH5PActivityData, siteId: string): Promise { + + const accessInfo = await AddonModH5PActivity.instance.getAccessInformation(h5pActivity.id, true, siteId); + + if (!accessInfo.canreviewattempts) { + // Not a teacher, prefetch user attempts and the current user profile. + const site = await this.sitesProvider.getSite(siteId); + + const options = { + ignoreCache: true, + siteId: siteId, + }; + + await Promise.all([ + AddonModH5PActivity.instance.getAllAttemptsResults(h5pActivity.id, options), + CoreUser.instance.prefetchProfiles([site.getUserId()], h5pActivity.course, siteId), + ]); + } + } +} diff --git a/src/addon/mod/h5pactivity/providers/report-link-handler.ts b/src/addon/mod/h5pactivity/providers/report-link-handler.ts new file mode 100644 index 000000000..615b151a7 --- /dev/null +++ b/src/addon/mod/h5pactivity/providers/report-link-handler.ts @@ -0,0 +1,147 @@ +// (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 { NavController } from 'ionic-angular'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; +import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate'; +import { CoreContentLinksHelper } from '@core/contentlinks/providers/helper'; +import { CoreCourse } from '@core/course/providers/course'; +import { AddonModH5PActivity } from './h5pactivity'; + +/** + * Handler to treat links to H5P activity report. + */ +@Injectable() +export class AddonModH5PActivityReportLinkHandler extends CoreContentLinksHandlerBase { + name = 'AddonModH5PActivityReportLinkHandler'; + featureName = 'CoreCourseModuleDelegate_AddonModH5PActivity'; + pattern = /\/mod\/h5pactivity\/report\.php.*([\&\?]a=\d+)/; + + constructor() { + super(); + } + + /** + * Get the list of actions for a link (url). + * + * @param siteIds List of sites the URL belongs to. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @return List of (or promise resolved with list of) actions. + */ + getActions(siteIds: string[], url: string, params: any, courseId?: number): + CoreContentLinksAction[] | Promise { + courseId = courseId || params.courseid || params.cid; + + return [{ + action: async (siteId, navCtrl?): Promise => { + try { + const id = Number(params.a); + + if (!courseId) { + courseId = await this.getCourseId(id, siteId); + } + + if (typeof params.attemptid != 'undefined') { + this.openAttemptResults(id, Number(params.attemptid), courseId, siteId, navCtrl); + } else { + const userId = params.userid ? Number(params.userid) : undefined; + + this.openUserAttempts(id, courseId, siteId, userId, navCtrl); + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'Error processing link.'); + } + } + }]; + } + + /** + * Get course Id for an activity. + * + * @param id Activity ID. + * @param siteId Site ID. + * @return Promise resolved with course ID. + */ + protected async getCourseId(id: number, siteId: string): Promise { + const modal = CoreDomUtils.instance.showModalLoading(); + + try { + const module = await CoreCourse.instance.getModuleBasicInfoByInstance(id, 'h5pactivity', siteId); + + return module.course; + } finally { + modal.dismiss(); + } + } + + /** + * Check if the handler is enabled for a certain site (site + user) and a URL. + * If not defined, defaults to true. + * + * @param siteId The site ID. + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param courseId Course ID related to the URL. Optional but recommended. + * @return Whether the handler is enabled for the URL and site. + */ + async isEnabled(siteId: string, url: string, params: any, courseId?: number): Promise { + return AddonModH5PActivity.instance.isPluginEnabled(); + } + + /** + * Open attempt results. + * + * @param id Activity ID. + * @param attemptId Attempt ID. + * @param courseId Course ID. + * @param siteId Site ID. + * @param navCtrl The NavController to use to navigate. + * @return Promise resolved when done. + */ + protected openAttemptResults(id: number, attemptId: number, courseId: number, siteId: string, navCtrl?: NavController): void { + + const pageParams = { + courseId: courseId, + h5pActivityId: id, + attemptId: attemptId, + }; + + CoreContentLinksHelper.instance.goInSite(navCtrl, 'AddonModH5PActivityAttemptResultsPage', pageParams, siteId); + } + + /** + * Open user attempts. + * + * @param id Activity ID. + * @param courseId Course ID. + * @param siteId Site ID. + * @param userId User ID. If not defined, current user in site. + * @param navCtrl The NavController to use to navigate. + * @return Promise resolved when done. + */ + protected openUserAttempts(id: number, courseId: number, siteId: string, userId?: number, navCtrl?: NavController): void { + + const pageParams = { + courseId: courseId, + h5pActivityId: id, + userId: userId, + }; + + CoreContentLinksHelper.instance.goInSite(navCtrl, 'AddonModH5PActivityUserAttemptsPage', pageParams, siteId); + } +} diff --git a/src/addon/mod/h5pactivity/providers/sync-cron-handler.ts b/src/addon/mod/h5pactivity/providers/sync-cron-handler.ts new file mode 100644 index 000000000..5ffccb86b --- /dev/null +++ b/src/addon/mod/h5pactivity/providers/sync-cron-handler.ts @@ -0,0 +1,46 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { CoreCronHandler } from '@providers/cron'; +import { AddonModH5PActivitySync } from './sync'; + +/** + * Synchronization cron handler. + */ +@Injectable() +export class AddonModH5PActivitySyncCronHandler implements CoreCronHandler { + name = 'AddonModH5PActivitySyncCronHandler'; + + /** + * Execute the process. + * Receives the ID of the site affected, undefined for all sites. + * + * @param siteId ID of the site affected, undefined for all sites. + * @param force Wether the execution is forced (manual sync). + * @return Promise resolved when done, rejected if failure. + */ + execute(siteId?: string, force?: boolean): Promise { + return AddonModH5PActivitySync.instance.syncAllActivities(siteId, force); + } + + /** + * Get the time between consecutive executions. + * + * @return Time between consecutive executions (in ms). + */ + getInterval(): number { + return AddonModH5PActivitySync.instance.syncInterval; + } +} diff --git a/src/addon/mod/h5pactivity/providers/sync.ts b/src/addon/mod/h5pactivity/providers/sync.ts new file mode 100644 index 000000000..dafa44842 --- /dev/null +++ b/src/addon/mod/h5pactivity/providers/sync.ts @@ -0,0 +1,223 @@ +// (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 { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEvents } from '@providers/events'; +import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreSyncProvider } from '@providers/sync'; +import { CoreUtils } from '@providers/utils/utils'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreCourse } from '@core/course/providers/course'; +import { CoreCourseLogHelper } from '@core/course/providers/log-helper'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; +import { CoreCourseActivitySyncBaseProvider } from '@core/course/classes/activity-sync'; +import { CoreXAPI } from '@core/xapi/providers/xapi'; +import { CoreXAPIOffline } from '@core/xapi/providers/offline'; +import { AddonModH5PActivity, AddonModH5PActivityProvider } from './h5pactivity'; +import { AddonModH5PActivityPrefetchHandler } from './prefetch-handler'; + +import { makeSingleton } from '@singletons/core.singletons'; + +/** + * Service to sync H5P activities. + */ +@Injectable() +export class AddonModH5PActivitySyncProvider extends CoreCourseActivitySyncBaseProvider { + + static AUTO_SYNCED = 'addon_mod_h5pactivity_autom_synced'; + protected componentTranslate: string; + + constructor(sitesProvider: CoreSitesProvider, + loggerProvider: CoreLoggerProvider, + appProvider: CoreAppProvider, + translate: TranslateService, + textUtils: CoreTextUtilsProvider, + syncProvider: CoreSyncProvider, + timeUtils: CoreTimeUtilsProvider, + prefetchHandler: AddonModH5PActivityPrefetchHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate) { + + super('AddonModH5PActivitySyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate, + timeUtils, prefetchDelegate, prefetchHandler); + + this.componentTranslate = CoreCourse.instance.translateModuleName('h5pactivity'); + } + + /** + * Try to synchronize all the H5P activities in a certain site or in all sites. + * + * @param siteId Site ID to sync. If not defined, sync all sites. + * @param force Wether to force sync not depending on last execution. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + syncAllActivities(siteId?: string, force?: boolean): Promise { + return this.syncOnSites('H5P activities', this.syncAllActivitiesFunc.bind(this), [force], siteId); + } + + /** + * Sync all H5P activities on a site. + * + * @param siteId Site ID to sync. If not defined, sync all sites. + * @param force Wether to force sync not depending on last execution. + * @return Promise resolved if sync is successful, rejected if sync fails. + */ + protected async syncAllActivitiesFunc(siteId?: string, force?: boolean): Promise { + const entries = await CoreXAPIOffline.instance.getAllStatements(siteId); + + // Sync all responses. + const promises = entries.map((response) => { + const promise = force ? this.syncActivity(response.contextid, siteId) : + this.syncActivityIfNeeded(response.contextid, siteId); + + return promise.then((result) => { + if (result && result.updated) { + // Sync successful, send event. + CoreEvents.instance.trigger(AddonModH5PActivitySyncProvider.AUTO_SYNCED, { + contextId: response.contextid, + warnings: result.warnings, + }, siteId); + } + }); + }); + + await Promise.all(promises); + } + + /** + * Sync an H5P activity only if a certain time has passed since the last time. + * + * @param contextId Context ID of the activity. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the activity is synced or it doesn't need to be synced. + */ + async syncActivityIfNeeded(contextId: number, siteId?: string): Promise { + const needed = await this.isSyncNeeded(contextId, siteId); + + if (needed) { + return this.syncActivity(contextId, siteId); + } + } + + /** + * Synchronize an H5P activity. If it's already being synced it will reuse the same promise. + * + * @param contextId Context ID of the activity. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + syncActivity(contextId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + if (!this.appProvider.isOnline()) { + // Cannot sync in offline. + throw this.translate.instant('core.networkerrormsg'); + } + + if (this.isSyncing(contextId, siteId)) { + // There's already a sync ongoing for this discussion, return the promise. + return this.getOngoingSync(contextId, siteId); + } + + return this.addOngoingSync(contextId, this.syncActivityData(contextId, siteId), siteId); + } + + /** + * Synchronize an H5P activity. + * + * @param contextId Context ID of the activity. + * @param siteId Site ID. + * @return Promise resolved if sync is successful, rejected otherwise. + */ + protected async syncActivityData(contextId: number, siteId: string): Promise<{warnings: string[], updated: boolean}> { + + this.logger.debug(`Try to sync H5P activity with context ID '${contextId}'`); + + const result = { + warnings: [], + updated: false, + }; + + // Get all the statements stored for the activity. + const entries = await CoreXAPIOffline.instance.getContextStatements(contextId, siteId); + + if (!entries || !entries.length) { + // Nothing to sync. + await this.setSyncTime(contextId, siteId); + + return result; + } + + // Get the activity instance. + const courseId = entries[0].courseid; + + const h5pActivity = await AddonModH5PActivity.instance.getH5PActivityByContextId(courseId, contextId, false, siteId); + + // Sync offline logs. + try { + await CoreCourseLogHelper.instance.syncIfNeeded(AddonModH5PActivityProvider.COMPONENT, h5pActivity.id, siteId); + } catch (error) { + // Ignore errors. + } + + // Send the statements in order. + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + + try { + await CoreXAPI.instance.postStatementsOnline(entry.component, entry.statements, siteId); + + result.updated = true; + + await CoreXAPIOffline.instance.deleteStatements(entry.id, siteId); + } catch (error) { + if (CoreUtils.instance.isWebServiceError(error)) { + // The WebService has thrown an error, this means that statements cannot be submitted. Delete them. + result.updated = true; + + await CoreXAPIOffline.instance.deleteStatements(entry.id, siteId); + + // Responses deleted, add a warning. + result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', { + component: this.componentTranslate, + name: entry.extra, + error: this.textUtils.getErrorMessageFromError(error), + })); + } else { + // Stop synchronizing. + throw error; + } + } + } + + if (result.updated) { + try { + // Data has been sent to server, invalidate attempts. + await AddonModH5PActivity.instance.invalidateUserAttempts(h5pActivity.id, undefined, siteId); + } catch (error) { + // Ignore errors. + } + } + + // Sync finished, set sync time. + await this.setSyncTime(contextId, siteId); + + return result; + } +} + +export class AddonModH5PActivitySync extends makeSingleton(AddonModH5PActivitySyncProvider) {} diff --git a/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html index 35cbae509..9f767d27c 100644 --- a/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html @@ -9,12 +9,17 @@ - + + + + + +
diff --git a/src/addon/mod/imscp/components/index/index.ts b/src/addon/mod/imscp/components/index/index.ts index a17aa8de4..246721aab 100644 --- a/src/addon/mod/imscp/components/index/index.ts +++ b/src/addon/mod/imscp/components/index/index.ts @@ -14,11 +14,10 @@ import { Component, Injector } from '@angular/core'; import { ModalController } from 'ionic-angular'; -import { CoreAppProvider } from '@providers/app'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; +import { + CoreCourseModuleMainResourceComponent, CoreCourseResourceDownloadResult +} from '@core/course/classes/main-resource-component'; import { AddonModImscpProvider } from '../../providers/imscp'; -import { AddonModImscpPrefetchHandler } from '../../providers/prefetch-handler'; /** * Component that displays a IMSCP. @@ -33,14 +32,15 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom items = []; currentItem: string; src = ''; + warning: string; // Initialize empty previous/next to prevent showing arrows for an instant before they're hidden. previousItem = ''; nextItem = ''; - constructor(injector: Injector, private imscpProvider: AddonModImscpProvider, private courseProvider: CoreCourseProvider, - private appProvider: CoreAppProvider, private modalCtrl: ModalController, - private imscpPrefetch: AddonModImscpPrefetchHandler) { + constructor(injector: Injector, + protected imscpProvider: AddonModImscpProvider, + protected modalCtrl: ModalController) { super(injector); } @@ -75,8 +75,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom * @return Promise resolved when done. */ protected fetchContent(refresh?: boolean): Promise { - let downloadFailed = false; - let downloadFailError; + let downloadResult: CoreCourseResourceDownloadResult; const promises = []; promises.push(this.imscpProvider.getImscp(this.courseId, this.module.id).then((imscp) => { @@ -84,17 +83,8 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom this.dataRetrieved.emit(imscp); })); - promises.push(this.imscpPrefetch.download(this.module, this.courseId).catch((error) => { - // Mark download as failed but go on since the main files could have been downloaded. - downloadFailed = true; - downloadFailError = error; - - return this.courseProvider.loadModuleContents(this.module, this.courseId).catch((error) => { - // Error getting module contents, fail. - this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); - - return Promise.reject(null); - }); + promises.push(this.downloadResourceIfNeeded(refresh).then((result) => { + downloadResult = result; })); return Promise.all(promises).then(() => { @@ -109,10 +99,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom return Promise.reject(null); }); }).then(() => { - if (downloadFailed && this.appProvider.isOnline()) { - // We could load the main file but the download failed. Show error message. - this.showErrorDownloadingSomeFiles(downloadFailError); - } + this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error) : ''; }).finally(() => { this.fillContextMenu(refresh); diff --git a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html index f7ac672e0..834abcf38 100644 --- a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/lesson/pages/player/player.ts b/src/addon/mod/lesson/pages/player/player.ts index d8fa3b011..2b2c1753b 100644 --- a/src/addon/mod/lesson/pages/player/player.ts +++ b/src/addon/mod/lesson/pages/player/player.ts @@ -18,7 +18,6 @@ import { IonicPage, NavParams, Content, PopoverController, ModalController, Moda import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; -import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreSyncProvider } from '@providers/sync'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -80,7 +79,7 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { protected loadingMenu: boolean; // Whether the lesson menu is being loaded. protected lessonPages: any[]; // Lesson pages (for the lesson menu). - constructor(protected navParams: NavParams, logger: CoreLoggerProvider, protected translate: TranslateService, + constructor(protected navParams: NavParams, protected translate: TranslateService, protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider, protected syncProvider: CoreSyncProvider, protected domUtils: CoreDomUtilsProvider, popoverCtrl: PopoverController, protected timeUtils: CoreTimeUtilsProvider, protected lessonProvider: AddonModLessonProvider, @@ -369,6 +368,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy { this.messages = this.messages.concat(data.messages); this.processData = undefined; + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'lesson' }); + // Format activity link if present. if (this.eolData && this.eolData.activitylink) { this.eolData.activitylink.value = this.lessonHelper.formatActivityLink(this.eolData.activitylink.value); diff --git a/src/addon/mod/lesson/providers/prefetch-handler.ts b/src/addon/mod/lesson/providers/prefetch-handler.ts index 46ef36db2..c8601ae58 100644 --- a/src/addon/mod/lesson/providers/prefetch-handler.ts +++ b/src/addon/mod/lesson/providers/prefetch-handler.ts @@ -111,7 +111,7 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan let files = lesson.mediafiles || []; files = files.concat(this.getIntroFilesFromInstance(module, lesson)); - return this.pluginFileDelegate.getFilesSize(files); + return this.pluginFileDelegate.getFilesDownloadSize(files); }).then((res) => { result = res; diff --git a/src/addon/mod/lti/components/index/index.ts b/src/addon/mod/lti/components/index/index.ts index ca933a8e5..f078e2070 100644 --- a/src/addon/mod/lti/components/index/index.ts +++ b/src/addon/mod/lti/components/index/index.ts @@ -16,6 +16,7 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; import { AddonModLtiProvider, AddonModLtiLti } from '../../providers/lti'; +import { AddonModLtiHelper } from '../../providers/helper'; /** * Component that displays an LTI entry page. @@ -92,18 +93,6 @@ export class AddonModLtiIndexComponent extends CoreCourseModuleMainActivityCompo * Launch the LTI. */ launch(): void { - this.ltiProvider.getLtiLaunchData(this.lti.id).then((launchData) => { - // "View" LTI. - this.ltiProvider.logView(this.lti.id, this.lti.name).then(() => { - this.checkCompletion(); - }).catch((error) => { - // Ignore errors. - }); - - // Launch LTI. - return this.ltiProvider.launch(launchData.endpoint, launchData.parameters); - }).catch((message) => { - this.domUtils.showErrorModalDefault(message, 'core.error', true); - }); + AddonModLtiHelper.instance.getDataAndLaunch(this.courseId, this.module, this.lti); } } diff --git a/src/addon/mod/lti/lti.module.ts b/src/addon/mod/lti/lti.module.ts index 4e2c83878..20649b794 100644 --- a/src/addon/mod/lti/lti.module.ts +++ b/src/addon/mod/lti/lti.module.ts @@ -16,6 +16,7 @@ import { NgModule } from '@angular/core'; import { AddonModLtiComponentsModule } from './components/components.module'; import { AddonModLtiModuleHandler } from './providers/module-handler'; import { AddonModLtiProvider } from './providers/lti'; +import { AddonModLtiHelperProvider } from './providers/helper'; import { AddonModLtiLinkHandler } from './providers/link-handler'; import { AddonModLtiListLinkHandler } from './providers/list-link-handler'; import { AddonModLtiPrefetchHandler } from './providers/prefetch-handler'; @@ -25,7 +26,8 @@ import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module- // List of providers (without handlers). export const ADDON_MOD_LTI_PROVIDERS: any[] = [ - AddonModLtiProvider + AddonModLtiProvider, + AddonModLtiHelperProvider, ]; @NgModule({ @@ -36,10 +38,11 @@ export const ADDON_MOD_LTI_PROVIDERS: any[] = [ ], providers: [ AddonModLtiProvider, + AddonModLtiHelperProvider, AddonModLtiModuleHandler, AddonModLtiLinkHandler, AddonModLtiListLinkHandler, - AddonModLtiPrefetchHandler + AddonModLtiPrefetchHandler, ] }) export class AddonModLtiModule { diff --git a/src/addon/mod/lti/providers/helper.ts b/src/addon/mod/lti/providers/helper.ts new file mode 100644 index 000000000..24a771c69 --- /dev/null +++ b/src/addon/mod/lti/providers/helper.ts @@ -0,0 +1,119 @@ +// (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 { Platform } from 'ionic-angular'; +import { CoreEvents, CoreEventsProvider } from '@providers/events'; +import { CoreSites } from '@providers/sites'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { CoreCourse } from '@core/course/providers/course'; +import { AddonModLti, AddonModLtiLti } from './lti'; + +import { makeSingleton } from '@singletons/core.singletons'; + +/** + * Service that provides some helper functions for LTI. + */ +@Injectable() +export class AddonModLtiHelperProvider { + + protected pendingCheckCompletion: {[moduleId: string]: {courseId: number, module: any}} = {}; + + constructor(platform: Platform) { + + platform.resume.subscribe(() => { + // User went back to the app, check pending completions. + for (const moduleId in this.pendingCheckCompletion) { + const data = this.pendingCheckCompletion[moduleId]; + + CoreCourse.instance.checkModuleCompletion(data.courseId, data.module.completiondata); + } + }); + + // Clear pending completion on logout. + CoreEvents.instance.on(CoreEventsProvider.LOGOUT, () => { + this.pendingCheckCompletion = {}; + }); + } + + /** + * Get needed data and launch the LTI. + * + * @param courseId Course ID. + * @param module Module. + * @param lti LTI instance. If not provided it will be obtained. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async getDataAndLaunch(courseId: number, module: any, lti?: AddonModLtiLti, siteId?: string): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + const modal = CoreDomUtils.instance.showModalLoading(); + + try { + const openInBrowser = await AddonModLti.instance.isOpenInAppBrowserDisabled(siteId); + + if (openInBrowser) { + const site = await CoreSites.instance.getSite(siteId); + + // The view event is triggered by the browser, mark the module as pending to check completion. + this.pendingCheckCompletion[module.id] = { + courseId, + module, + }; + + await site.openInBrowserWithAutoLogin(module.url); + } else { + // Open in app. + if (!lti) { + lti = await AddonModLti.instance.getLti(courseId, module.id); + } + + const launchData = await AddonModLti.instance.getLtiLaunchData(lti.id); + + // "View" LTI without blocking the UI. + this.logViewAndCheckCompletion(courseId, module, lti.id, lti.name, siteId); + + // Launch LTI. + return AddonModLti.instance.launch(launchData.endpoint, launchData.parameters); + } + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_lti.errorgetlti', true); + } finally { + modal.dismiss(); + } + } + + /** + * Report the LTI as being viewed and check completion. + * + * @param courseId Course ID. + * @param module Module. + * @param ltiId LTI id. + * @param name Name of the lti. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when done. + */ + async logViewAndCheckCompletion(courseId: number, module: any, ltiId: number, name?: string, siteId?: string): Promise { + try { + await AddonModLti.instance.logView(ltiId, name); + + CoreCourse.instance.checkModuleCompletion(courseId, module.completiondata); + } catch (error) { + // Ignore errors. + } + } +} + +export class AddonModLtiHelper extends makeSingleton(AddonModLtiHelperProvider) {} diff --git a/src/addon/mod/lti/providers/lti.ts b/src/addon/mod/lti/providers/lti.ts index 0c544b4b6..acae1b745 100644 --- a/src/addon/mod/lti/providers/lti.ts +++ b/src/addon/mod/lti/providers/lti.ts @@ -24,6 +24,8 @@ import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper'; import { CoreSite } from '@classes/site'; import { CoreWSExternalWarning, CoreWSExternalFile } from '@providers/ws'; +import { makeSingleton } from '@singletons/core.singletons'; + /** * Service that provides some features for LTI. */ @@ -59,9 +61,9 @@ export class AddonModLtiProvider { * @param params Launch params. * @return Promise resolved with the file URL. */ - generateLauncher(url: string, params: AddonModLtiParam[]): Promise { + async generateLauncher(url: string, params: AddonModLtiParam[]): Promise { if (!this.fileProvider.isAvailable()) { - return Promise.resolve(url); + return url; } // Generate a form with the params. @@ -84,13 +86,13 @@ export class AddonModLtiProvider { ' }; \n' + ' \n'; - return this.fileProvider.writeFile(this.LAUNCHER_FILE_NAME, text).then((entry) => { - if (this.appProvider.isDesktop()) { - return entry.toInternalURL(); - } else { - return entry.toURL(); - } - }); + const entry = await this.fileProvider.writeFile(this.LAUNCHER_FILE_NAME, text); + + if (this.appProvider.isDesktop()) { + return entry.toInternalURL(); + } else { + return entry.toURL(); + } } /** @@ -193,6 +195,30 @@ export class AddonModLtiProvider { return this.sitesProvider.getCurrentSite().invalidateWsCacheForKey(this.getLtiLaunchDataCacheKey(id)); } + /** + * Check if open in InAppBrowser is disabled. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether it's disabled. + */ + async isOpenInAppBrowserDisabled(siteId?: string): Promise { + const site = await this.sitesProvider.getSite(siteId); + + return this.isOpenInAppBrowserDisabledInSite(site); + } + + /** + * Check if open in InAppBrowser is disabled. + * + * @param site Site. If not defined, current site. + * @return Whether it's disabled. + */ + isOpenInAppBrowserDisabledInSite(site?: CoreSite): boolean { + site = site || this.sitesProvider.getCurrentSite(); + + return site.isFeatureDisabled('CoreCourseModuleDelegate_AddonModLti:openInAppBrowser'); + } + /** * Launch LTI. * @@ -200,20 +226,20 @@ export class AddonModLtiProvider { * @param params Launch params. * @return Promise resolved when the WS call is successful. */ - launch(url: string, params: AddonModLtiParam[]): Promise { + async launch(url: string, params: AddonModLtiParam[]): Promise { if (!this.urlUtils.isHttpURL(url)) { - return Promise.reject(this.translate.instant('addon.mod_lti.errorinvalidlaunchurl')); + throw this.translate.instant('addon.mod_lti.errorinvalidlaunchurl'); } // Generate launcher and open it. - return this.generateLauncher(url, params).then((url) => { - if (this.appProvider.isMobile()) { - this.utils.openInApp(url); - } else { - // In desktop open in browser, we found some cases where inapp caused JS issues. - this.utils.openInBrowser(url); - } - }); + const launcherUrl = await this.generateLauncher(url, params); + + if (this.appProvider.isMobile()) { + this.utils.openInApp(launcherUrl); + } else { + // In desktop open in browser, we found some cases where inapp caused JS issues. + this.utils.openInBrowser(launcherUrl); + } } /** @@ -233,6 +259,8 @@ export class AddonModLtiProvider { } } +export class AddonModLti extends makeSingleton(AddonModLtiProvider) {} + /** * LTI returned by mod_lti_get_ltis_by_courses. */ diff --git a/src/addon/mod/lti/providers/module-handler.ts b/src/addon/mod/lti/providers/module-handler.ts index e6aa945c5..eba92ce10 100644 --- a/src/addon/mod/lti/providers/module-handler.ts +++ b/src/addon/mod/lti/providers/module-handler.ts @@ -18,11 +18,11 @@ import { DomSanitizer } from '@angular/platform-browser'; import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; import { CoreAppProvider } from '@providers/app'; import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreFilepoolProvider } from '@providers/filepool'; import { CoreSitesProvider } from '@providers/sites'; import { AddonModLtiIndexComponent } from '../components/index/index'; import { AddonModLtiProvider } from './lti'; +import { AddonModLtiHelper } from './helper'; import { CoreConstants } from '@core/constants'; /** @@ -46,7 +46,6 @@ export class AddonModLtiModuleHandler implements CoreCourseModuleHandler { constructor(private appProvider: CoreAppProvider, private courseProvider: CoreCourseProvider, - private domUtils: CoreDomUtilsProvider, private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider, private ltiProvider: AddonModLtiProvider, @@ -85,26 +84,8 @@ export class AddonModLtiModuleHandler implements CoreCourseModuleHandler { icon: 'link', label: 'addon.mod_lti.launchactivity', action: (event: Event, navCtrl: NavController, module: any, courseId: number): void => { - const modal = this.domUtils.showModalLoading(); - - // Get LTI and launch data. - this.ltiProvider.getLti(courseId, module.id).then((ltiData) => { - return this.ltiProvider.getLtiLaunchData(ltiData.id).then((launchData) => { - // "View" LTI. - this.ltiProvider.logView(ltiData.id, ltiData.name).then(() => { - this.courseProvider.checkModuleCompletion(courseId, module.completiondata); - }).catch(() => { - // Ignore errors. - }); - - // Launch LTI. - return this.ltiProvider.launch(launchData.endpoint, launchData.parameters); - }); - }).catch((message) => { - this.domUtils.showErrorModalDefault(message, 'addon.mod_lti.errorgetlti', true); - }).finally(() => { - modal.dismiss(); - }); + // Launch the LTI. + AddonModLtiHelper.instance.getDataAndLaunch(courseId, module); } }] }; diff --git a/src/addon/mod/page/components/index/addon-mod-page-index.html b/src/addon/mod/page/components/index/addon-mod-page-index.html index 378796f11..d3d6c7726 100644 --- a/src/addon/mod/page/components/index/addon-mod-page-index.html +++ b/src/addon/mod/page/components/index/addon-mod-page-index.html @@ -6,7 +6,7 @@ - + @@ -15,6 +15,10 @@ + + + +

diff --git a/src/addon/mod/page/components/index/index.ts b/src/addon/mod/page/components/index/index.ts index b8300246a..27f84b9b9 100644 --- a/src/addon/mod/page/components/index/index.ts +++ b/src/addon/mod/page/components/index/index.ts @@ -13,13 +13,12 @@ // limitations under the License. import { Component, Injector } from '@angular/core'; -import { CoreAppProvider } from '@providers/app'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; +import { + CoreCourseModuleMainResourceComponent, CoreCourseResourceDownloadResult +} from '@core/course/classes/main-resource-component'; import { AddonModPageProvider, AddonModPagePage } from '../../providers/page'; import { AddonModPageHelperProvider } from '../../providers/helper'; -import { AddonModPagePrefetchHandler } from '../../providers/prefetch-handler'; /** * Component that displays a page. @@ -35,12 +34,14 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp displayDescription = true; displayTimemodified = true; page: AddonModPagePage; + warning: string; protected fetchContentDefaultError = 'addon.mod_page.errorwhileloadingthepage'; - constructor(injector: Injector, private pageProvider: AddonModPageProvider, private courseProvider: CoreCourseProvider, - private appProvider: CoreAppProvider, private pageHelper: AddonModPageHelperProvider, - private pagePrefetch: AddonModPagePrefetchHandler, private utils: CoreUtilsProvider) { + constructor(injector: Injector, + protected pageProvider: AddonModPageProvider, + protected pageHelper: AddonModPageHelperProvider, + protected utils: CoreUtilsProvider) { super(injector); } @@ -77,19 +78,11 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp * @return Promise resolved when done. */ protected fetchContent(refresh?: boolean): Promise { - let downloadFailed = false; - let downloadFailError; + let downloadResult: CoreCourseResourceDownloadResult; - // Download content. This function also loads module contents if needed. - return this.pagePrefetch.download(this.module, this.courseId).catch((error) => { - // Mark download as failed but go on since the main files could have been downloaded. - downloadFailed = true; - downloadFailError = error; - }).then(() => { - if (!this.module.contents.length) { - // Try to load module contents for offline usage. - return this.courseProvider.loadModuleContents(this.module, this.courseId); - } + // Download the resource if it needs to be downloaded. + return this.downloadResourceIfNeeded(refresh).then((result) => { + downloadResult = result; }).then(() => { const promises = []; @@ -131,11 +124,7 @@ export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComp promises.push(this.pageHelper.getPageHtml(this.module.contents, this.module.id).then((content) => { this.contents = content; - - if (downloadFailed && this.appProvider.isOnline()) { - // We could load the main file but the download failed. Show error message. - this.showErrorDownloadingSomeFiles(downloadFailError); - } + this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error) : ''; })); return Promise.all(promises); diff --git a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html index 88de59576..ec8685448 100644 --- a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html +++ b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/quiz/components/index/index.scss b/src/addon/mod/quiz/components/index/index.scss index e359e8a0b..f6f7cb907 100644 --- a/src/addon/mod/quiz/components/index/index.scss +++ b/src/addon/mod/quiz/components/index/index.scss @@ -46,6 +46,12 @@ ion-app.app-root addon-mod-quiz-index { background-color: $blue-dark; color: $blue-light; } + + .item.addon-mod_quiz-highlighted.activated, + .item.addon-mod_quiz-highlighted.activated p { + background-color: $blue; + color: $blue-light; + } } } } diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts index 908918fbf..653a9b89a 100644 --- a/src/addon/mod/quiz/pages/player/player.ts +++ b/src/addon/mod/quiz/pages/player/player.ts @@ -376,6 +376,8 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy { synced: !this.offline }, this.sitesProvider.getCurrentSiteId()); + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'quiz' }); + // Leave the player. this.forceLeave = true; this.navCtrl.pop(); diff --git a/src/addon/mod/resource/components/index/addon-mod-resource-index.html b/src/addon/mod/resource/components/index/addon-mod-resource-index.html index 7d2e36a94..e6aa1efff 100644 --- a/src/addon/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addon/mod/resource/components/index/addon-mod-resource-index.html @@ -6,7 +6,7 @@ - + @@ -15,6 +15,10 @@ + + + + diff --git a/src/addon/mod/resource/components/index/index.ts b/src/addon/mod/resource/components/index/index.ts index 8875bae06..58810ad3d 100644 --- a/src/addon/mod/resource/components/index/index.ts +++ b/src/addon/mod/resource/components/index/index.ts @@ -13,14 +13,12 @@ // limitations under the License. import { Component, Injector } from '@angular/core'; -import { CoreAppProvider } from '@providers/app'; import { CoreFilepoolProvider } from '@providers/filepool'; -import { CoreSitesProvider } from '@providers/sites'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; +import { + CoreCourseModuleMainResourceComponent, CoreCourseResourceDownloadResult +} from '@core/course/classes/main-resource-component'; import { AddonModResourceProvider } from '../../providers/resource'; -import { AddonModResourcePrefetchHandler } from '../../providers/prefetch-handler'; import { AddonModResourceHelperProvider } from '../../providers/helper'; /** @@ -38,14 +36,11 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource src: string; contentText: string; displayDescription = true; + warning: string; constructor(injector: Injector, protected resourceProvider: AddonModResourceProvider, - protected courseProvider: CoreCourseProvider, - protected appProvider: CoreAppProvider, - protected prefetchHandler: AddonModResourcePrefetchHandler, protected resourceHelper: AddonModResourceHelperProvider, - protected sitesProvider: CoreSitesProvider, protected utils: CoreUtilsProvider, protected filepoolProvider: CoreFilepoolProvider) { super(injector); @@ -109,13 +104,10 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource } if (this.resourceHelper.isDisplayedInIframe(this.module)) { - let downloadFailed = false; - let downloadFailError; + let downloadResult: CoreCourseResourceDownloadResult; - return this.prefetchHandler.download(this.module, this.courseId).catch((error) => { - // Mark download as failed but go on since the main files could have been downloaded. - downloadFailed = true; - downloadFailError = error; + return this.downloadResourceIfNeeded(refresh, true).then((result) => { + downloadResult = result; }).then(() => { return this.resourceHelper.getIframeSrc(this.module).then((src) => { this.mode = 'iframe'; @@ -131,14 +123,12 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource this.src = src; } - if (downloadFailed && this.appProvider.isOnline()) { - // We could load the main file but the download failed. Show error message. - this.showErrorDownloadingSomeFiles(downloadFailError); - } + this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error) : ''; }); }); } else if (this.resourceHelper.isDisplayedEmbedded(this.module, resource && resource.display)) { this.mode = 'embedded'; + this.warning = ''; return this.resourceHelper.getEmbeddedHtml(this.module, this.courseId).then((html) => { this.contentText = html; @@ -147,6 +137,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource }); } else { this.mode = 'external'; + this.warning = ''; } }).finally(() => { this.fillContextMenu(refresh); @@ -159,7 +150,7 @@ export class AddonModResourceIndexComponent extends CoreCourseModuleMainResource * @return Promise resolved when done. */ async open(): Promise { - let downloadable = await this.prefetchHandler.isDownloadable(this.module, this.courseId); + let downloadable = await this.modulePrefetchDelegate.isModuleDownloadable(this.module, this.courseId); if (downloadable) { // Check if the main file is downloadle. diff --git a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html index a9fcfd058..cc16da2c2 100644 --- a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html +++ b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html @@ -7,7 +7,7 @@ - + @@ -98,7 +98,7 @@

- + ({{ 'addon.mod_scorm.score' | translate }}: {{sco.score_raw}})

@@ -130,43 +130,42 @@ - -
- -

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

-
- - {{ 'addon.mod_scorm.browse' | translate }} - - - - {{ 'addon.mod_scorm.normal' | translate }} - - -
- - - - {{ 'addon.mod_scorm.newattempt' | translate }} - - - - - + + + {{ 'addon.mod_scorm.newattempt' | translate }} + + + + -

{{ statusMessage | translate }}

-
- - {{ 'addon.mod_scorm.enter' | translate }} +

{{ statusMessage | translate }}

+ + + + + + + + + + + +
-

{{ progressMessage | translate }}

-

{{ 'core.percentagenumber' | translate:{$a: percentage} }}

+

{{ progressMessage | translate }}

+
diff --git a/src/addon/mod/scorm/components/index/index.ts b/src/addon/mod/scorm/components/index/index.ts index f2fd0a9a2..e16a6484a 100644 --- a/src/addon/mod/scorm/components/index/index.ts +++ b/src/addon/mod/scorm/components/index/index.ts @@ -37,12 +37,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom scorm: any; // The SCORM object. currentOrganization: any = {}; // Selected organization. - scormOptions: any = { // Options to open the SCORM. - mode: AddonModScormProvider.MODENORMAL, - newAttempt: false - }; - modeNormal = AddonModScormProvider.MODENORMAL; // Normal open mode. - modeBrowser = AddonModScormProvider.MODEBROWSE; // Browser open mode. + startNewAttempt = false; errorMessage: string; // Error message. syncTime: string; // Last sync time. hasOffline: boolean; // Whether the SCORM has offline data. @@ -223,7 +218,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom if (this.scorm.forcenewattempt == AddonModScormProvider.SCORM_FORCEATTEMPT_ALWAYS || (this.scorm.forcenewattempt && !this.scorm.incomplete)) { - this.scormOptions.newAttempt = true; + this.startNewAttempt = true; } promises.push(this.getReportedGrades()); @@ -372,7 +367,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom if (this.hasPlayed) { this.hasPlayed = false; - this.scormOptions.newAttempt = false; // Uncheck new attempt. + this.startNewAttempt = false; // Uncheck new attempt. // Add a delay to make sure the player has started the last writing calls so we can detect conflicts. setTimeout(() => { @@ -492,7 +487,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom * @param event Event. * @param scoId SCO that needs to be loaded when the SCORM is opened. If not defined, load first SCO. */ - open(event?: Event, scoId?: number): void { + open(event?: Event, preview: boolean = false, scoId?: number): void { if (event) { event.preventDefault(); event.stopPropagation(); @@ -515,7 +510,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom this.downloadScormPackage().then(() => { // Success downloading, open SCORM if user hasn't left the view. if (!this.isDestroyed) { - this.openScorm(scoId); + this.openScorm(scoId, preview); } }).catch((error) => { if (!this.isDestroyed) { @@ -526,7 +521,7 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom }); }); } else { - this.openScorm(scoId); + this.openScorm(scoId, preview); } } @@ -535,11 +530,11 @@ export class AddonModScormIndexComponent extends CoreCourseModuleMainActivityCom * * @param scoId SCO ID. */ - protected openScorm(scoId: number): void { + protected openScorm(scoId: number, preview: boolean = false): void { this.navCtrl.push('AddonModScormPlayerPage', { scorm: this.scorm, - mode: this.scormOptions.mode, - newAttempt: !!this.scormOptions.newAttempt, + mode: preview ? AddonModScormProvider.MODEBROWSE : AddonModScormProvider.MODENORMAL, + newAttempt: !!this.startNewAttempt, organizationId: this.currentOrganization.identifier, scoId: scoId }); diff --git a/src/addon/mod/scorm/lang/en.json b/src/addon/mod/scorm/lang/en.json index e7d6e5cfb..3e234713d 100644 --- a/src/addon/mod/scorm/lang/en.json +++ b/src/addon/mod/scorm/lang/en.json @@ -32,12 +32,10 @@ "highestattempt": "Highest attempt", "incomplete": "Incomplete", "lastattempt": "Last completed attempt", - "mode": "Mode", "modulenameplural": "SCORM packages", "newattempt": "Start a new attempt", "noattemptsallowed": "Number of attempts allowed", "noattemptsmade": "Number of attempts you have made", - "normal": "Normal", "notattempted": "Not attempted", "offlineattemptnote": "This attempt has data that hasn't been synchronised.", "offlineattemptovermax": "This attempt cannot be sent because you exceeded the maximum number of attempts.", diff --git a/src/addon/mod/scorm/pages/player/player.ts b/src/addon/mod/scorm/pages/player/player.ts index a7b507809..2b689387d 100644 --- a/src/addon/mod/scorm/pages/player/player.ts +++ b/src/addon/mod/scorm/pages/player/player.ts @@ -310,6 +310,8 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy { * Page will leave. */ ionViewWillUnload(): void { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'scorm' }); + // Empty src when leaving the state so unload event is triggered in the iframe. this.src = ''; } diff --git a/src/addon/mod/scorm/providers/prefetch-handler.ts b/src/addon/mod/scorm/providers/prefetch-handler.ts index 0d3756312..d8263dbd7 100644 --- a/src/addon/mod/scorm/providers/prefetch-handler.ts +++ b/src/addon/mod/scorm/providers/prefetch-handler.ts @@ -220,6 +220,7 @@ export class AddonModScormPrefetchHandler extends CoreCourseActivityPrefetchHand /** * Function that converts a regular ProgressEvent into a AddonModScormProgressEvent. * + * @param downloading True when downloading, false when unzipping. * @param onProgress Function to call on progress. * @param progress Event returned by the download function. */ diff --git a/src/addon/mod/scorm/providers/scorm.ts b/src/addon/mod/scorm/providers/scorm.ts index a8424dc3f..2cec079ec 100644 --- a/src/addon/mod/scorm/providers/scorm.ts +++ b/src/addon/mod/scorm/providers/scorm.ts @@ -22,6 +22,7 @@ import { CoreSyncProvider } from '@providers/sync'; import { CoreWSProvider } from '@providers/ws'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUrlUtils } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { AddonModScormOfflineProvider } from './scorm-offline'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; @@ -1256,7 +1257,7 @@ export class AddonModScormProvider { /** * Invalidates access information. * - * @param forumId SCORM ID. + * @param scormId SCORM ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ @@ -1403,7 +1404,7 @@ export class AddonModScormProvider { protected isExternalLink(link: string): boolean { link = link.toLowerCase(); - if (link.match(/https?:\/\//)) { + if (link.match(/^https?:\/\//i) && !CoreUrlUtils.instance.isLocalFileUrl(link)) { return true; } else if (link.substr(0, 4) == 'www.') { return true; @@ -1543,7 +1544,7 @@ export class AddonModScormProvider { return this.logHelper.logSingle('mod_scorm_view_scorm', params, AddonModScormProvider.COMPONENT, id, name, 'scorm', {}, siteId); -} + } /** * Saves a SCORM tracking record. diff --git a/src/addon/mod/survey/components/index/addon-mod-survey-index.html b/src/addon/mod/survey/components/index/addon-mod-survey-index.html index 42c41ddd0..eb101c594 100644 --- a/src/addon/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addon/mod/survey/components/index/addon-mod-survey-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/survey/components/index/index.ts b/src/addon/mod/survey/components/index/index.ts index 633957845..b7e71e144 100644 --- a/src/addon/mod/survey/components/index/index.ts +++ b/src/addon/mod/survey/components/index/index.ts @@ -15,6 +15,7 @@ import { Component, Optional, Injector } from '@angular/core'; import { Content } from 'ionic-angular'; import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component'; +import { CoreEvents, CoreEventsProvider } from '@providers/events'; import { AddonModSurveyProvider, AddonModSurveySurvey } from '../../providers/survey'; import { AddonModSurveyHelperProvider, AddonModSurveyQuestionFormatted } from '../../providers/helper'; import { AddonModSurveyOfflineProvider } from '../../providers/offline'; @@ -38,9 +39,14 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo protected userId: number; protected syncEventName = AddonModSurveySyncProvider.AUTO_SYNCED; - constructor(injector: Injector, private surveyProvider: AddonModSurveyProvider, @Optional() content: Content, - private surveyHelper: AddonModSurveyHelperProvider, private surveyOffline: AddonModSurveyOfflineProvider, - private surveySync: AddonModSurveySyncProvider) { + constructor( + injector: Injector, + protected surveyProvider: AddonModSurveyProvider, + @Optional() content: Content, + protected surveyHelper: AddonModSurveyHelperProvider, + protected surveyOffline: AddonModSurveyOfflineProvider, + protected surveySync: AddonModSurveySyncProvider, + ) { super(injector, content); } @@ -185,6 +191,8 @@ export class AddonModSurveyIndexComponent extends CoreCourseModuleMainActivityCo } return this.surveyProvider.submitAnswers(this.survey.id, this.survey.name, this.courseId, answers).then((online) => { + CoreEvents.instance.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: this.moduleName }); + if (online && this.isPrefetched()) { // The survey is downloaded, update the data. return this.surveySync.prefetchAfterUpdate(this.module, this.courseId).then(() => { diff --git a/src/addon/mod/url/components/index/index.ts b/src/addon/mod/url/components/index/index.ts index b9381aae0..1275b3796 100644 --- a/src/addon/mod/url/components/index/index.ts +++ b/src/addon/mod/url/components/index/index.ts @@ -13,9 +13,7 @@ // limitations under the License. import { Component, Injector } from '@angular/core'; -import { CoreSitesProvider } from '@providers/sites'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; -import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component'; import { AddonModUrlProvider } from '../../providers/url'; import { AddonModUrlHelperProvider } from '../../providers/helper'; @@ -43,9 +41,10 @@ export class AddonModUrlIndexComponent extends CoreCourseModuleMainResourceCompo mimetype: string; displayDescription = true; - constructor(injector: Injector, private urlProvider: AddonModUrlProvider, private courseProvider: CoreCourseProvider, - private urlHelper: AddonModUrlHelperProvider, private mimeUtils: CoreMimetypeUtilsProvider, - private sitesProvider: CoreSitesProvider) { + constructor(injector: Injector, + protected urlProvider: AddonModUrlProvider, + protected urlHelper: AddonModUrlHelperProvider, + protected mimeUtils: CoreMimetypeUtilsProvider) { super(injector); } diff --git a/src/addon/mod/url/providers/module-handler.ts b/src/addon/mod/url/providers/module-handler.ts index 151dd2a8c..7d15e61fc 100644 --- a/src/addon/mod/url/providers/module-handler.ts +++ b/src/addon/mod/url/providers/module-handler.ts @@ -118,7 +118,7 @@ export class AddonModUrlModuleHandler implements CoreCourseModuleHandler { buttons: [ { hidden: true, // Hide it until we calculate if it should be displayed or not. icon: 'link', - label: 'core.openinbrowser', + label: 'core.openmodinbrowser', action: (event: Event, navCtrl: NavController, module: any, courseId: number): void => { handler.openUrl(module, courseId); } diff --git a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html index e043a6299..44a57c1ca 100644 --- a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html @@ -19,7 +19,7 @@ - + diff --git a/src/addon/mod/wiki/pages/edit/edit.ts b/src/addon/mod/wiki/pages/edit/edit.ts index 01edf08ae..b0bf66db0 100644 --- a/src/addon/mod/wiki/pages/edit/edit.ts +++ b/src/addon/mod/wiki/pages/edit/edit.ts @@ -465,6 +465,8 @@ export class AddonModWikiEditPage implements OnInit, OnDestroy { this.domUtils.triggerFormSubmittedEvent(this.formElement, id > 0, this.sitesProvider.getCurrentSiteId()); if (id > 0) { + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'wiki' }); + // Page was created, get its data and go to the page. this.pageId = id; diff --git a/src/addon/mod/wiki/providers/prefetch-handler.ts b/src/addon/mod/wiki/providers/prefetch-handler.ts index 7870b158d..f9677e893 100644 --- a/src/addon/mod/wiki/providers/prefetch-handler.ts +++ b/src/addon/mod/wiki/providers/prefetch-handler.ts @@ -97,7 +97,7 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl siteId = this.sitesProvider.getCurrentSiteId(); promises.push(this.getFiles(module, courseId, single, siteId).then((files) => { - return this.pluginFileDelegate.getFilesSize(files); + return this.pluginFileDelegate.getFilesDownloadSize(files); })); promises.push(this.getAllPages(module, courseId, false, true, siteId).then((pages) => { diff --git a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html index 3ce7e2c32..c10216d25 100644 --- a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html +++ b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts index 809419793..6d4f6d704 100644 --- a/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts +++ b/src/addon/mod/workshop/pages/edit-submission/edit-submission.ts @@ -388,6 +388,8 @@ export class AddonModWorkshopEditSubmissionPage implements OnInit, OnDestroy { data['submissionId'] = newSubmissionId; } + this.eventsProvider.trigger(CoreEventsProvider.ACTIVITY_DATA_SENT, { module: 'workshop' }); + const promise = newSubmissionId ? this.workshopProvider.invalidateSubmissionData(this.workshopId, newSubmissionId) : Promise.resolve(); diff --git a/src/addon/mod/workshop/pages/submission/submission.ts b/src/addon/mod/workshop/pages/submission/submission.ts index aab6470db..245d320ac 100644 --- a/src/addon/mod/workshop/pages/submission/submission.ts +++ b/src/addon/mod/workshop/pages/submission/submission.ts @@ -234,16 +234,16 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { this.canDelete = !assessment; } - assessment.userid = assessment.reviewerid; - assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); - - if (this.currentUserId == assessment.userid) { - this.ownAssessment = assessment; - assessment.ownAssessment = true; - } + assessment = this.parseAssessment(assessment); this.submissionInfo.reviewedby = [assessment]; })); + } else if (this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED && this.userId == this.currentUserId) { + this.workshopProvider.getSubmissionAssessments(this.workshopId, this.submissionId).then((assessments) => { + this.submissionInfo.reviewedby = assessments.map((assessment) => { + return this.parseAssessment(assessment); + }); + }); } if (this.canAddFeedback || this.workshop.phase == AddonModWorkshopProvider.PHASE_CLOSED) { @@ -324,6 +324,24 @@ export class AddonModWorkshopSubmissionPage implements OnInit, OnDestroy { }); } + /** + * Parse assessment to be shown. + * + * @param assessment Original assessment. + * @return Parsed assessment. + */ + protected parseAssessment(assessment: any): any { + assessment.userid = assessment.reviewerid; + assessment = this.workshopHelper.realGradeValue(this.workshop, assessment); + + if (this.currentUserId == assessment.userid) { + this.ownAssessment = assessment; + assessment.ownAssessment = true; + } + + return assessment; + } + /** * Force leaving the page, without checking for changes. */ diff --git a/src/addon/qtype/ddimageortext/classes/ddimageortext.ts b/src/addon/qtype/ddimageortext/classes/ddimageortext.ts index e3d1533bb..8e303a71b 100644 --- a/src/addon/qtype/ddimageortext/classes/ddimageortext.ts +++ b/src/addon/qtype/ddimageortext/classes/ddimageortext.ts @@ -133,10 +133,8 @@ export class AddonQtypeDdImageOrTextQuestion { const dragNode = this.doc.cloneNewDragItem(i, dragItemNo); i++; - if (!this.readOnly) { - // Make the item draggable. - this.draggableForQuestion(dragNode, group, choice); - } + // Make the item draggable. + this.draggableForQuestion(dragNode, group, choice); // If the draggable item needs to be created more than once, create the rest of copies. if (dragNode.classList.contains('infinite')) { @@ -146,9 +144,8 @@ export class AddonQtypeDdImageOrTextQuestion { while (dragsToCreate > 0) { const newDragNode = this.doc.cloneNewDragItem(i, dragItemNo); i++; - if (!this.readOnly) { - this.draggableForQuestion(newDragNode, group, choice); - } + this.draggableForQuestion(newDragNode, group, choice); + dragsToCreate--; } } @@ -202,11 +199,15 @@ export class AddonQtypeDdImageOrTextQuestion { let dragItemsArea = topNode.querySelector('div.draghomes'); if (dragItemsArea) { + // On 3.9+ dragitems were removed. + const dragItems = topNode.querySelector('div.dragitems'); + + if (dragItems) { + // Remove empty div.dragitems. + dragItems.remove(); + } + // 3.6+ site, transform HTML so it has the same structure as in Moodle 3.5. - - // Remove empty div.dragitems. - topNode.querySelector('div.dragitems').remove(); - const ddArea = topNode.querySelector('div.ddarea'); // Move div.dropzones to div.ddarea. @@ -332,17 +333,19 @@ export class AddonQtypeDdImageOrTextQuestion { drag.setAttribute('group', String(group)); drag.setAttribute('choice', String(choice)); - // Listen to click events. - drag.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); + if (!this.readOnly) { + // Listen to click events. + drag.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); - if (drag.classList.contains('beingdragged')) { - this.deselectDrags(); - } else { - this.selectDrag(drag); - } - }); + if (drag.classList.contains('beingdragged')) { + this.deselectDrags(); + } else { + this.selectDrag(drag); + } + }); + } } /** @@ -387,14 +390,9 @@ export class AddonQtypeDdImageOrTextQuestion { getUnplacedChoiceForDrop(choice: number, drop: HTMLElement): HTMLElement { const dragItems = this.getChoicesForDrop(choice, drop); - for (let x = 0; x < dragItems.length; x++) { - const dragItem = dragItems[x]; - if (this.readOnly || (!dragItem.classList.contains('placed') && !dragItem.classList.contains('beingdragged'))) { - return dragItem; - } - } - - return null; + return dragItems.find((dragItem) => { + return (!dragItem.classList.contains('placed') && !dragItem.classList.contains('beingdragged')); + }) || null; } /** @@ -530,7 +528,7 @@ export class AddonQtypeDdImageOrTextQuestion { if (originInputId && originInputId != targetInputId) { // Remove it from the previous place. const originInputNode = this.doc.topNode().querySelector('input#' + originInputId); - originInputNode.setAttribute('value', ''); + originInputNode.setAttribute('value', '0'); } // Now position the draggable and set it to the input. @@ -539,7 +537,10 @@ export class AddonQtypeDdImageOrTextQuestion { drag.style.top = position[1] - 1 + 'px'; drag.classList.add('placed'); - inputNode.setAttribute('value', drag.getAttribute('choice')); + if (drag.getAttribute('choice')) { + inputNode.setAttribute('value', drag.getAttribute('choice')); + } + drag.setAttribute('inputid', targetInputId); } @@ -575,7 +576,7 @@ export class AddonQtypeDdImageOrTextQuestion { const inputId = drag.getAttribute('inputid'); if (inputId) { const inputNode = this.doc.topNode().querySelector('input#' + inputId); - inputNode.setAttribute('value', ''); + inputNode.setAttribute('value', '0'); } // Move the element to its original position. @@ -652,6 +653,7 @@ export class AddonQtypeDdImageOrTextQuestion { if (choice > 0) { const dragItem = this.getUnplacedChoiceForDrop(choice, dropZone); + if (dragItem !== null) { this.placeDragInDrop(dragItem, dropZone); } diff --git a/src/addon/qtype/ddimageortext/component/ddimageortext.scss b/src/addon/qtype/ddimageortext/component/ddimageortext.scss index 9523b78ab..2bd859fed 100644 --- a/src/addon/qtype/ddimageortext/component/ddimageortext.scss +++ b/src/addon/qtype/ddimageortext/component/ddimageortext.scss @@ -35,6 +35,28 @@ addon-qtype-ddimageortext { } } + .group2 { + border-radius: 10px 0 0 0; + } + .group3 { + border-radius: 0 10px 0 0; + } + .group4 { + border-radius: 0 0 10px 0; + } + .group5 { + border-radius: 0 0 0 10px; + } + .group6 { + border-radius: 0 10px 10px 0; + } + .group7 { + border-radius: 10px 0 0 10px; + } + .group8 { + border-radius: 10px 10px 10px 10px; + } + .drag { border: 1px solid $gray-darker; color: $text-color; diff --git a/src/addon/qtype/ddmarker/classes/ddmarker.ts b/src/addon/qtype/ddmarker/classes/ddmarker.ts index d5199c656..90b5686b1 100644 --- a/src/addon/qtype/ddmarker/classes/ddmarker.ts +++ b/src/addon/qtype/ddmarker/classes/ddmarker.ts @@ -27,6 +27,7 @@ export interface AddonQtypeDdMarkerQuestionDocStructure { dragItems?: () => HTMLElement[]; dragItemsForChoice?: (choiceNo: number) => HTMLElement[]; dragItemForChoice?: (choiceNo: number, itemNo: number) => HTMLElement; + dragItemPlaceholder?: (choiceNo: number) => HTMLElement; dragItemBeingDragged?: (choiceNo: number) => HTMLElement; dragItemHome?: (choiceNo: number) => HTMLElement; dragItemHomes?: () => HTMLElement[]; @@ -36,6 +37,14 @@ export interface AddonQtypeDdMarkerQuestionDocStructure { markerTexts?: () => HTMLElement; } +/** + * Point type. + */ +export type AddonQtypeDdMarkerQuestionPoint = { + x: number; // X axis coordinates. + y: number; // Y axis coordinates. +}; + /** * Class to make a question of ddmarker type work. */ @@ -98,14 +107,13 @@ export class AddonQtypeDdMarkerQuestion { * @return The new element. */ cloneNewDragItem(dragHome: HTMLElement, itemNo: number): HTMLElement { - const marker = dragHome.querySelector('span.markertext'); - marker.style.opacity = '0.6'; - // Clone the element and add the right classes. const drag = dragHome.cloneNode(true); drag.classList.remove('draghome'); drag.classList.add('dragitem'); drag.classList.add('item' + itemNo); + drag.classList.remove('dragplaceholder'); // In case it has it. + dragHome.classList.add('dragplaceholder'); // Insert the new drag after the dragHome. dragHome.parentElement.insertBefore(drag, dragHome.nextSibling); @@ -122,15 +130,14 @@ export class AddonQtypeDdMarkerQuestion { * @param bgImgXY X and Y of the BG IMG relative position. * @return Position relative to the window. */ - convertToWindowXY(bgImgXY: number[]): number[] { - const bgImg = this.doc.bgImg(), - position = this.domUtils.getElementXY(bgImg, null, 'ddarea'); + convertToWindowXY(bgImgXY: string): number[] { + const bgImg = this.doc.bgImg(); + const position = this.domUtils.getElementXY(bgImg, null, 'ddarea'); + let coordsNumbers = this.parsePoint(bgImgXY); - // Render the position related to the current image dimensions. - bgImgXY[0] *= this.proportion; - bgImgXY[1] *= this.proportion; + coordsNumbers = this.makePointProportional(coordsNumbers); - return [Number(bgImgXY[0]) + position[0], Number(bgImgXY[1]) + position[1]]; + return [coordsNumbers.x + position[0], coordsNumbers.y + position[1]]; } /** @@ -139,10 +146,10 @@ export class AddonQtypeDdMarkerQuestion { * @param coords Coordinates to check. * @return Whether they're inside the background image. */ - coordsInImg(coords: number[]): boolean { + coordsInImg(coords: AddonQtypeDdMarkerQuestionPoint): boolean { const bgImg = this.doc.bgImg(); - return (coords[0] * this.proportion <= bgImg.width && coords[1] * this.proportion <= bgImg.height); + return (coords.x * this.proportion <= bgImg.width + 1) && (coords.y * this.proportion <= bgImg.height + 1); } /** @@ -173,7 +180,7 @@ export class AddonQtypeDdMarkerQuestion { */ docStructure(slot: number): AddonQtypeDdMarkerQuestionDocStructure { const topNode = this.container.querySelector('.addon-qtype-ddmarker-container'), - dragItemsArea = topNode.querySelector('div.dragitems'); + dragItemsArea = topNode.querySelector('div.dragitems, div.draghomes'); return { topNode: (): HTMLElement => { @@ -194,14 +201,18 @@ export class AddonQtypeDdMarkerQuestion { dragItemForChoice: (choiceNo: number, itemNo: number): HTMLElement => { return dragItemsArea.querySelector('span.dragitem.choice' + choiceNo + '.item' + itemNo); }, + dragItemPlaceholder: (choiceNo: number): HTMLElement => { + return dragItemsArea.querySelector('span.dragplaceholder.choice' + choiceNo); + }, dragItemBeingDragged: (choiceNo: number): HTMLElement => { return dragItemsArea.querySelector('span.dragitem.beingdragged.choice' + choiceNo); }, dragItemHome: (choiceNo: number): HTMLElement => { - return dragItemsArea.querySelector('span.draghome.choice' + choiceNo); + return dragItemsArea.querySelector('span.draghome.choice' + choiceNo + + ', span.marker.choice' + choiceNo); }, dragItemHomes: (): HTMLElement[] => { - return Array.from(dragItemsArea.querySelectorAll('span.draghome')); + return Array.from(dragItemsArea.querySelectorAll('span.draghome, span.marker')); }, getClassnameNumericSuffix: (node: HTMLElement, prefix: string): number => { @@ -328,13 +339,11 @@ export class AddonQtypeDdMarkerQuestion { const markerSpan = this.doc.topNode().querySelector( 'div.ddarea div.markertexts span.markertext' + dropZoneNo); if (markerSpan !== null) { - - xyForText[0] = (xyForText[0] - markerSpan.offsetWidth / 2) * this.proportion; - xyForText[1] = (xyForText[1] - markerSpan.offsetHeight / 2) * this.proportion; - + const width = this.domUtils.getElementMeasure(markerSpan, true, true, false, true); + const height = this.domUtils.getElementMeasure(markerSpan, false, true, false, true); markerSpan.style.opacity = '0.6'; - markerSpan.style.left = xyForText[0] + 'px'; - markerSpan.style.top = xyForText[1] + 'px'; + markerSpan.style.left = (xyForText.x - (width / 2)) + 'px'; + markerSpan.style.top = (xyForText.y - (height / 2)) + 'px'; const markerSpanAnchor = markerSpan.querySelector('a'); if (markerSpanAnchor !== null) { @@ -364,38 +373,36 @@ export class AddonQtypeDdMarkerQuestion { * Draw a circle in a drop zone. * * @param dropZoneNo Number of the drop zone. - * @param coords Coordinates of the circle. + * @param coordinates Coordinates of the circle. * @param colour Colour of the circle. * @return X and Y position of the center of the circle. */ - drawShapeCircle(dropZoneNo: number, coords: string, colour: string): number[] { - // Extract the numbers in the coordinates. - const coordsParts = coords.match(/(\d+),(\d+);(\d+)/); + drawShapeCircle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint { + if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?$/)) { + return null; + } - if (coordsParts && coordsParts.length === 4) { - // Remove first element and convert them to number. - coordsParts.shift(); + const bits = coordinates.split(';'); + let centre = this.parsePoint(bits[0]); + const radius = Number(bits[1]); - const coordsPartsNum = coordsParts.map((i) => { - return Number(i); + // Calculate circle limits and check it's inside the background image. + const circleLimit = {x: centre.x - radius, y: centre.y - radius}; + if (this.coordsInImg(circleLimit)) { + centre = this.makePointProportional(centre); + + // All good, create the shape. + this.shapes[dropZoneNo] = this.graphics.addShape({ + type: 'circle', + color: colour + }, { + cx: centre.x, + cy: centre.y, + r: Math.round(radius * this.proportion) }); - // Calculate circle limits and check it's inside the background image. - const circleLimit = [coordsPartsNum[0] - coordsPartsNum[2], coordsPartsNum[1] - coordsPartsNum[2]]; - if (this.coordsInImg(circleLimit)) { - // All good, create the shape. - this.shapes[dropZoneNo] = this.graphics.addShape({ - type: 'circle', - color: colour - }, { - cx: coordsPartsNum[0] * this.proportion, - cy: coordsPartsNum[1] * this.proportion, - r: coordsPartsNum[2] * this.proportion - }); - - // Return the center. - return [coordsPartsNum[0], coordsPartsNum[1]]; - } + // Return the centre. + return centre; } return null; @@ -405,39 +412,40 @@ export class AddonQtypeDdMarkerQuestion { * Draw a rectangle in a drop zone. * * @param dropZoneNo Number of the drop zone. - * @param coords Coordinates of the rectangle. + * @param coordinates Coordinates of the rectangle. * @param colour Colour of the rectangle. * @return X and Y position of the center of the rectangle. */ - drawShapeRectangle(dropZoneNo: number, coords: string, colour: string): number[] { - // Extract the numbers in the coordinates. - const coordsParts = coords.match(/(\d+),(\d+);(\d+),(\d+)/); + drawShapeRectangle(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint { + if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?;\d+(\.\d+)?,\d+(\.\d+)?$/)) { + return null; + } - if (coordsParts && coordsParts.length === 5) { - // Remove first element and convert them to number. - coordsParts.shift(); + const bits = coordinates.split(';'); + const startPoint = this.parsePoint(bits[0]); + const size = this.parsePoint(bits[1]); - const coordsPartsNum = coordsParts.map((i) => { - return Number(i); + // Calculate rectangle limits and check it's inside the background image. + const rectLimits = {x: startPoint.x + size.x, y: startPoint.y + size.y}; + if (this.coordsInImg(rectLimits)) { + const startPointProp = this.makePointProportional(startPoint); + const sizeProp = this.makePointProportional(size); + + // All good, create the shape. + this.shapes[dropZoneNo] = this.graphics.addShape({ + type: 'rect', + color: colour + }, { + x: startPointProp.x, + y: startPointProp.y, + width: sizeProp.x, + height: sizeProp.y }); - // Calculate rectangle limits and check it's inside the background image. - const rectLimits = [coordsPartsNum[0] + coordsPartsNum[2], coordsPartsNum[1] + coordsPartsNum[3]]; - if (this.coordsInImg(rectLimits)) { - // All good, create the shape. - this.shapes[dropZoneNo] = this.graphics.addShape({ - type: 'rect', - color: colour - }, { - x: coordsPartsNum[0] * this.proportion, - y: coordsPartsNum[1] * this.proportion, - width: coordsPartsNum[2] * this.proportion, - height: coordsPartsNum[3] * this.proportion - }); + const centre = { x: startPoint.x + (size.x / 2) , y: startPoint.y + (size.y / 2)}; - // Return the center. - return [coordsPartsNum[0] + coordsPartsNum[2] / 2, coordsPartsNum[1] + coordsPartsNum[3] / 2]; - } + // Return the centre. + return this.makePointProportional(centre); } return null; @@ -447,53 +455,83 @@ export class AddonQtypeDdMarkerQuestion { * Draw a polygon in a drop zone. * * @param dropZoneNo Number of the drop zone. - * @param coords Coordinates of the polygon. + * @param coordinates Coordinates of the polygon. * @param colour Colour of the polygon. * @return X and Y position of the center of the polygon. */ - drawShapePolygon(dropZoneNo: number, coords: string, colour: string): number[] { - const coordsParts = coords.split(';'), - points = [], - bgImg = this.doc.bgImg(), - maxXY = [0, 0], - minXY = [bgImg.width, bgImg.height]; - - for (const i in coordsParts) { - // Extract the X and Y of this point. - const partsString = coordsParts[i].match(/^(\d+),(\d+)$/), - parts = partsString && partsString.map((part) => { - return Number(part); - }); - - if (parts !== null && this.coordsInImg([parts[1], parts[2]])) { - parts[1] *= this.proportion; - parts[2] *= this.proportion; - - // Calculate min and max points to find center to show marker on. - minXY[0] = Math.min(parts[1], minXY[0]); - minXY[1] = Math.min(parts[2], minXY[1]); - maxXY[0] = Math.max(parts[1], maxXY[0]); - maxXY[1] = Math.max(parts[2], maxXY[1]); - - points.push(parts[1] + ',' + parts[2]); - } + drawShapePolygon(dropZoneNo: number, coordinates: string, colour: string): AddonQtypeDdMarkerQuestionPoint { + if (!coordinates.match(/^\d+(\.\d+)?,\d+(\.\d+)?(?:;\d+(\.\d+)?,\d+(\.\d+)?)*$/)) { + return null; } - if (points.length > 2) { + const bits = coordinates.split(';'); + const centre = {x: 0, y: 0}; + const points = bits.map((bit) => { + const point = this.parsePoint(bit); + centre.x += point.x; + centre.y += point.y; + + return point; + }); + + if (points.length > 0) { + centre.x = Math.round(centre.x / points.length); + centre.y = Math.round(centre.y / points.length); + } + + const pointsOnImg = []; + points.forEach((point) => { + if (this.coordsInImg(point)) { + point = this.makePointProportional(point); + + pointsOnImg.push(point.x + ',' + point.y); + } + }); + + if (pointsOnImg.length > 2) { this.shapes[dropZoneNo] = this.graphics.addShape({ type: 'polygon', color: colour }, { - points: points.join(' ') + points: pointsOnImg.join(' ') }); - // Return the center. - return [(minXY[0] + maxXY[0]) / 2, (minXY[1] + maxXY[1]) / 2]; + // Return the centre. + return this.makePointProportional(centre); } return null; } + /** + * Make a point from the string representation. + * + * @param coordinates "x,y". + * @return Coordinates to the point. + */ + parsePoint(coordinates: string): AddonQtypeDdMarkerQuestionPoint { + const bits = coordinates.split(','); + if (bits.length !== 2) { + throw coordinates + ' is not a valid point'; + } + + return {x: Number(bits[0]), y: Number(bits[1])}; + } + + /** + * Make proportional position of the point. + * + * @param point Point coordinates. + * @return Converted point. + */ + makePointProportional(point: AddonQtypeDdMarkerQuestionPoint): AddonQtypeDdMarkerQuestionPoint { + return { + x: Math.round(point.x * this.proportion), + y: Math.round(point.y * this.proportion) + + }; + } + /** * Drop a drag element into a certain position. * @@ -507,9 +545,6 @@ export class AddonQtypeDdMarkerQuestion { // Set the position related to the natural image dimensions. if (this.proportion < 1) { position[0] = Math.round(position[0] / this.proportion); - } - - if (this.proportion < 1) { position[1] = Math.round(position[1] / this.proportion); } } @@ -538,11 +573,7 @@ export class AddonQtypeDdMarkerQuestion { const coordsStrings = fv.split(';'); for (let i = 0; i < coordsStrings.length; i++) { - const coordsNumbers = coordsStrings[i].split(',').map((i) => { - return Number(i); - }); - - coords[coords.length] = this.convertToWindowXY(coordsNumbers); + coords[coords.length] = this.convertToWindowXY(coordsStrings[i]); } } @@ -581,9 +612,6 @@ export class AddonQtypeDdMarkerQuestion { // Set the position related to the natural image dimensions. if (this.proportion < 1) { position[0] = Math.round(position[0] / this.proportion); - } - - if (this.proportion < 1) { position[1] = Math.round(position[1] / this.proportion); } @@ -723,6 +751,12 @@ export class AddonQtypeDdMarkerQuestion { this.question.loaded = true; }; + if (bgImg.complete && bgImg.naturalWidth) { + imgLoaded(); + + return; + } + bgImg.addEventListener('load', imgLoaded); // Try again after a while. @@ -764,13 +798,25 @@ export class AddonQtypeDdMarkerQuestion { dragItem.classList.remove('unneeded'); } + const placeholder = this.doc.dragItemPlaceholder(choiceNo); + // Remove the class only if is placed on the image. if (homePosition[0] != coords[i][0] || homePosition[1] != coords[i][1]) { dragItem.classList.remove('unplaced'); - } + dragItem.classList.add('placed'); - dragItem.style.left = coords[i][0] + 'px'; - dragItem.style.top = coords[i][1] + 'px'; + const computedStyle = getComputedStyle(dragItem); + const left = coords[i][0] - this.domUtils.getComputedStyleMeasure(computedStyle, 'marginLeft'); + const top = coords[i][1] - this.domUtils.getComputedStyleMeasure(computedStyle, 'marginTop'); + + dragItem.style.left = left + 'px'; + dragItem.style.top = top + 'px'; + placeholder.classList.add('active'); + } else { + dragItem.classList.remove('placed'); + dragItem.classList.add('unplaced'); + placeholder.classList.remove('active'); + } } } diff --git a/src/addon/qtype/ddmarker/component/ddmarker.scss b/src/addon/qtype/ddmarker/component/ddmarker.scss index cba59c4e7..7c33d5302 100644 --- a/src/addon/qtype/ddmarker/component/ddmarker.scss +++ b/src/addon/qtype/ddmarker/component/ddmarker.scss @@ -9,24 +9,15 @@ addon-qtype-ddmarker { display: block; } + .droparea { + display: inline-block; + } + div.droparea img { border: 1px solid $gray-darker; max-width: 100%; } - .draghome img, .draghome span { - visibility: hidden; - } - - .dragitems .dragitem { - cursor: pointer; - position: absolute; - z-index: 2; - } - - .dropzones { - position: absolute; - } .dropzones svg { z-index: 3; } @@ -35,31 +26,81 @@ addon-qtype-ddmarker { z-index: 5; box-shadow: $core-dd-question-selected-shadow; } - .dragitems .draghome { - margin: 10px; - display: inline-block; + + .dragitems, // Previous to 3.9. + .draghomes { + &.readonly { + .dragitem, + .marker { + cursor: auto; + } + } + + .dragitem, // Previous to 3.9. + .draghome, + .marker { + vertical-align: top; + cursor: pointer; + position: relative; + margin: 10px; + display: inline-block; + &.dragplaceholder { + display: none; + visibility: hidden; + + &.active { + display: inline-block; + } + } + + &.unplaced { + position: relative; + } + &.placed { + position: absolute; + opacity: 0.6; + } + } } - .dragitems.readonly .dragitem { - cursor: auto; + .droparea { + .dragitem, + .marker { + cursor: pointer; + position: absolute; + vertical-align: top; + z-index: 2; + } } + div.ddarea { text-align: center; + position: relative; } + div.ddarea .dropzones, div.ddarea .markertexts { + top: 0; + left: 0; min-height: 80px; position: absolute; @include text-align('start'); } + .dropbackground { margin: 0 auto; } - div.dragitems div.draghome, div.dragitems div.dragitem, - div.draghome, div.drag { + div.dragitems div.draghome, + div.dragitems div.dragitem, + div.draghome, + div.drag, + div.draghomes div.marker, + div.marker, + div.drag { font: 13px/1.231 arial,helvetica,clean,sans-serif; } div.dragitems span.markertext, + div.draghomes span.markertext, div.markertexts span.markertext { margin: 0 5px; z-index: 2; @@ -86,17 +127,18 @@ addon-qtype-ddmarker { border-color: $yellow; padding: 5px; border-radius: 10px; - filter: alpha(opacity=60); opacity: 0.6; margin: 5px; display: inline-block; } - div.dragitems img.target { + div.dragitems img.target, + div.draghomes img.target { position: absolute; left: -7px; /* This must be half the size of the target image, minus 0.5. */ top: -7px; /* In other words, this works for a 15x15 cross-hair. */ } - div.dragitems div.draghome img.target { + div.dragitems div.draghome img.target, + div.draghomes div.marker img.target { display: none; } } diff --git a/src/addon/qtype/ddmarker/component/ddmarker.ts b/src/addon/qtype/ddmarker/component/ddmarker.ts index a7d30837c..1b0870b36 100644 --- a/src/addon/qtype/ddmarker/component/ddmarker.ts +++ b/src/addon/qtype/ddmarker/component/ddmarker.ts @@ -89,14 +89,20 @@ export class AddonQtypeDdMarkerComponent extends CoreQuestionBaseComponent imple } } else if (this.question.amdArgs) { // Moodle version >= 3.6. - if (typeof this.question.amdArgs[1] != 'undefined') { - this.imgSrc = this.question.amdArgs[1]; + let nextIndex = 1; + // Moodle version >= 3.9, imgSrc is not specified, do not advance index. + if (typeof this.question.amdArgs[nextIndex] != 'undefined' && typeof this.question.amdArgs[nextIndex] != 'boolean') { + this.imgSrc = this.question.amdArgs[nextIndex]; + nextIndex++; } - if (typeof this.question.amdArgs[2] != 'undefined') { - this.question.readOnly = this.question.amdArgs[2]; + + if (typeof this.question.amdArgs[nextIndex] != 'undefined') { + this.question.readOnly = this.question.amdArgs[nextIndex]; } - if (typeof this.question.amdArgs[3] != 'undefined') { - this.dropZones = this.question.amdArgs[3]; + nextIndex++; + + if (typeof this.question.amdArgs[nextIndex] != 'undefined') { + this.dropZones = this.question.amdArgs[nextIndex]; } } diff --git a/src/addon/qtype/ddwtos/component/ddwtos.scss b/src/addon/qtype/ddwtos/component/ddwtos.scss index 30a103686..6bce91256 100644 --- a/src/addon/qtype/ddwtos/component/ddwtos.scss +++ b/src/addon/qtype/ddwtos/component/ddwtos.scss @@ -33,7 +33,6 @@ addon-qtype-ddwtos { } .draghome, .drag.unplaced{ border: 1px solid $gray-darker; - border-radius: 5px; } .draghome { visibility: hidden; @@ -89,6 +88,28 @@ addon-qtype-ddwtos { } } + .group2 { + border-radius: 10px 0 0 0; + } + .group3 { + border-radius: 0 10px 0 0; + } + .group4 { + border-radius: 0 0 10px 0; + } + .group5 { + border-radius: 0 0 0 10px; + } + .group6 { + border-radius: 0 10px 10px 0; + } + .group7 { + border-radius: 10px 0 0 10px; + } + .group8 { + border-radius: 10px 10px 10px 10px; + } + sub, sup { font-size: 80%; position: relative; diff --git a/src/addon/storagemanager/lang/en.json b/src/addon/storagemanager/lang/en.json index a3491b8b9..8efc60a30 100644 --- a/src/addon/storagemanager/lang/en.json +++ b/src/addon/storagemanager/lang/en.json @@ -1,5 +1,6 @@ { "deletecourse": "Offload all course data", + "deletecourses": "Offload all courses data", "deletedatafrom": "Offload data from {{name}}", "info": "Files stored on your device make the app work faster and enable the app to be used offline. You can safely offload files if you need to free up storage space.", "managestorage": "Manage storage", diff --git a/src/addon/storagemanager/pages/course-storage/course-storage.ts b/src/addon/storagemanager/pages/course-storage/course-storage.ts index ff9196403..6bdeecc52 100644 --- a/src/addon/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addon/storagemanager/pages/course-storage/course-storage.ts @@ -98,7 +98,17 @@ export class AddonStorageManagerCourseStoragePage { * * (This works by deleting data for each module on the course that has data.) */ - deleteForCourse(): void { + async deleteForCourse(): Promise { + try { + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + const modules = []; this.sections.forEach((section) => { section.modules.forEach((module) => { @@ -118,7 +128,17 @@ export class AddonStorageManagerCourseStoragePage { * * @param section Section object with information about section and modules */ - deleteForSection(section: any): void { + async deleteForSection(section: any): Promise { + try { + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + const modules = []; section.modules.forEach((module) => { if (module.totalSize > 0) { @@ -134,10 +154,22 @@ export class AddonStorageManagerCourseStoragePage { * * @param module Module details */ - deleteForModule(module: any): void { - if (module.totalSize > 0) { - this.deleteModules([module]); + async deleteForModule(module: any): Promise { + if (module.totalSize === 0) { + return; } + + try { + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + + this.deleteModules([module]); } /** diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.html b/src/addon/storagemanager/pages/courses-storage/courses-storage.html new file mode 100644 index 000000000..937ddd2be --- /dev/null +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.html @@ -0,0 +1,40 @@ + + + {{ 'addon.storagemanager.managestorage' | translate }} + + + + + + +

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

+

{{ 'addon.storagemanager.info' | translate }}

+ + +

{{ 'addon.storagemanager.storageused' | translate }}

+
+

{{ totalSize | coreBytesToSize }}

+
+ +
+
+
+ + + +

{{ course.displayname }}

+

{{ 'core.downloading' | translate }}

+

+ + {{ course.totalSize | coreBytesToSize }} +

+ +
+
+
+
+
diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.module.ts b/src/addon/storagemanager/pages/courses-storage/courses-storage.module.ts new file mode 100644 index 000000000..95a210991 --- /dev/null +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.module.ts @@ -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 { NgModule } from '@angular/core'; +import { IonicPageModule } from 'ionic-angular'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; +import { CorePipesModule } from '@pipes/pipes.module'; +import { AddonStorageManagerCoursesStoragePage } from './courses-storage'; + +@NgModule({ + declarations: [ + AddonStorageManagerCoursesStoragePage, + ], + imports: [ + CoreComponentsModule, + CoreDirectivesModule, + CorePipesModule, + IonicPageModule.forChild(AddonStorageManagerCoursesStoragePage), + TranslateModule.forChild() + ], +}) +export class AddonStorageManagerCoursesStoragePageModule { +} diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.scss b/src/addon/storagemanager/pages/courses-storage/courses-storage.scss new file mode 100644 index 000000000..b26c3fb94 --- /dev/null +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.scss @@ -0,0 +1,28 @@ +ion-app.app-root page-addon-storagemanager-courses-storage { + + .item-md.item-block .item-inner { + padding-right: 0; + padding-left: 0; + } + + ion-item.course { + border-bottom: 1px solid $list-border-color; + padding-right: 16px; + padding-left: 16px; + + h2 { + font-weight: bold; + font-size: 2rem; + } + + h3 { + color: $subdued-text-color; + } + + &:last-child { + border-bottom: 0; + } + + } + +} diff --git a/src/addon/storagemanager/pages/courses-storage/courses-storage.ts b/src/addon/storagemanager/pages/courses-storage/courses-storage.ts new file mode 100644 index 000000000..05b5aa709 --- /dev/null +++ b/src/addon/storagemanager/pages/courses-storage/courses-storage.ts @@ -0,0 +1,218 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component } from '@angular/core'; +import { IonicPage } from 'ionic-angular'; +import { CoreCourse, CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourses } from '@core/courses/providers/courses'; +import { CoreArray } from '@singletons/array'; +import { CoreCourseModulePrefetch } from '@core/course/providers/module-prefetch-delegate'; +import { CoreConstants } from '@core/constants'; +import { CoreDomUtils } from '@providers/utils/dom'; +import { Translate } from '@singletons/core.singletons'; +import { CoreEvents, CoreEventsProvider, CoreEventObserver } from '@providers/events'; +import { CoreCourseHelper } from '@core/course/providers/helper'; + +/** + * Core course data. + */ +interface Course { + id: number; + displayname: string; +} + +/** + * Downloaded course data. + */ +interface DownloadedCourse extends Course { + totalSize: number; + isDownloading: boolean; +} + +/** + * Page that displays downloaded courses and allows the user to delete them. + */ +@IonicPage({ segment: 'addon-storagemanager-courses-storage' }) +@Component({ + selector: 'page-addon-storagemanager-courses-storage', + templateUrl: 'courses-storage.html', +}) +export class AddonStorageManagerCoursesStoragePage { + + userCourses: Course[] = []; + downloadedCourses: DownloadedCourse[] = []; + completelyDownloadedCourses: DownloadedCourse[] = []; + totalSize = 0; + loaded = false; + + courseStatusObserver: CoreEventObserver; + + /** + * View loaded. + */ + async ionViewDidLoad(): Promise { + this.userCourses = await CoreCourses.instance.getUserCourses(); + this.courseStatusObserver = CoreEvents.instance.on( + CoreEventsProvider.COURSE_STATUS_CHANGED, + ({ courseId, status }) => this.onCourseUpdated(courseId, status), + ); + + const downloadedCourseIds = await CoreCourse.instance.getDownloadedCourseIds(); + const downloadedCourses = await Promise.all( + this.userCourses + .filter((course) => downloadedCourseIds.indexOf(course.id) !== -1) + .map((course) => this.getDownloadedCourse(course)), + ); + + this.setDownloadedCourses(downloadedCourses); + + this.loaded = true; + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.courseStatusObserver && this.courseStatusObserver.off(); + } + + /** + * Delete all courses that have been downloaded. + */ + async deleteCompletelyDownloadedCourses(): Promise { + try { + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + + const modal = CoreDomUtils.instance.showModalLoading(); + const deletedCourseIds = this.completelyDownloadedCourses.map((course) => course.id); + + try { + await Promise.all(deletedCourseIds.map((courseId) => CoreCourseHelper.instance.deleteCourseFiles(courseId))); + + this.setDownloadedCourses(this.downloadedCourses.filter((course) => !CoreArray.contains(deletedCourseIds, course.id))); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, Translate.instance.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + + /** + * Delete course. + * + * @param course Course to delete. + */ + async deleteCourse(course: DownloadedCourse): Promise { + try { + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + } catch (error) { + if (!error.coreCanceled) { + throw error; + } + + return; + } + + const modal = CoreDomUtils.instance.showModalLoading(); + + try { + await CoreCourseHelper.instance.deleteCourseFiles(course.id); + + this.setDownloadedCourses(CoreArray.withoutItem(this.downloadedCourses, course)); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, Translate.instance.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + + /** + * Handle course updated event. + * + * @param courseId Updated course id. + */ + private async onCourseUpdated(courseId: number, status: string): Promise { + if (courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { + this.setDownloadedCourses([]); + + return; + } + + const course = this.downloadedCourses.find((course) => course.id === courseId); + + if (!course) { + return; + } + + course.isDownloading = status === CoreConstants.DOWNLOADING; + course.totalSize = await this.calculateDownloadedCourseSize(course.id); + + this.setDownloadedCourses(this.downloadedCourses); + } + + /** + * Set downloaded courses data. + * + * @param courses Courses info. + */ + private setDownloadedCourses(courses: DownloadedCourse[]): void { + this.downloadedCourses = courses; + this.completelyDownloadedCourses = courses.filter((course) => !course.isDownloading); + this.totalSize = this.downloadedCourses.reduce((totalSize, course) => totalSize + course.totalSize, 0); + } + + /** + * Get downloaded course data. + * + * @param course Course. + * @return Course info. + */ + private async getDownloadedCourse(course: Course): Promise { + const totalSize = await this.calculateDownloadedCourseSize(course.id); + const status = await CoreCourse.instance.getCourseStatus(course.id); + + return { + ...course, + totalSize, + isDownloading: status === CoreConstants.DOWNLOADING, + }; + } + + /** + * Calculate the size of a downloaded course. + * + * @param courseId Downloaded course id. + * @return Promise to be resolved with the course size. + */ + private async calculateDownloadedCourseSize(courseId: number): Promise { + const sections = await CoreCourse.instance.getSections(courseId); + const modules = CoreArray.flatten(sections.map((section) => section.modules)); + const promisedModuleSizes = modules.map(async (module) => { + const size = await CoreCourseModulePrefetch.instance.getModuleDownloadedSize(module, courseId); + + return isNaN(size) ? 0 : size; + }); + const moduleSizes = await Promise.all(promisedModuleSizes); + + return moduleSizes.reduce((totalSize, moduleSize) => totalSize + moduleSize, 0); + } + +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8316ea5fb..434d6925b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -22,7 +22,7 @@ import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider } from '@providers/sites'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; -import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; +import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes'; import { CoreLoginHelperProvider } from '@core/login/providers/helper'; import { Keyboard } from '@ionic-native/keyboard'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; @@ -140,7 +140,9 @@ export class MoodleMobileApp implements OnInit { if (this.urlSchemesProvider.isCustomURL(url)) { // Close the browser if it's a valid SSO URL. - this.urlSchemesProvider.handleCustomURL(url); + this.urlSchemesProvider.handleCustomURL(url).catch((error: CoreCustomURLSchemesHandleError) => { + this.urlSchemesProvider.treatHandleCustomURLError(error); + }); this.utils.closeInAppBrowser(false); } else if (this.platform.is('android')) { @@ -189,12 +191,19 @@ export class MoodleMobileApp implements OnInit { return; } + if (!this.urlSchemesProvider.isCustomURL(url)) { + // Not a custom URL, ignore. + return; + } + this.logger.debug('App launched by URL ', url); this.lastUrls[url] = Date.now(); this.eventsProvider.trigger(CoreEventsProvider.APP_LAUNCHED_URL, url); - this.urlSchemesProvider.handleCustomURL(url); + this.urlSchemesProvider.handleCustomURL(url).catch((error: CoreCustomURLSchemesHandleError) => { + this.urlSchemesProvider.treatHandleCustomURLError(error); + }); }); }; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 81fdb84a7..d5080dccb 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -14,7 +14,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgModule, COMPILER_OPTIONS } from '@angular/core'; +import { NgModule, COMPILER_OPTIONS, Injector } from '@angular/core'; import { IonicApp, IonicModule, Platform, Content, ScrollEvent, Config, Refresher } from 'ionic-angular'; import { assert } from 'ionic-angular/util/util'; import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; @@ -88,6 +88,7 @@ import { CoreFilterModule } from '@core/filter/filter.module'; import { CoreH5PModule } from '@core/h5p/h5p.module'; import { CoreSearchModule } from '@core/search/search.module'; import { CoreEditorModule } from '@core/editor/editor.module'; +import { CoreXAPIModule } from '@core/xapi/xapi.module'; // Addon modules. import { AddonBadgesModule } from '@addon/badges/badges.module'; @@ -98,6 +99,7 @@ import { AddonCourseCompletionModule } from '@addon/coursecompletion/coursecompl import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module'; import { AddonFilesModule } from '@addon/files/files.module'; import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module'; +import { AddonBlockActivityResultsModule } from '@addon/block/activityresults/activityresults.module'; import { AddonBlockBadgesModule } from '@addon/block/badges/badges.module'; import { AddonBlockBlogMenuModule } from '@addon/block/blogmenu/blogmenu.module'; import { AddonBlockBlogTagsModule } from '@addon/block/blogtags/blogtags.module'; @@ -154,6 +156,9 @@ import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module'; import { AddonQtypeModule } from '@addon/qtype/qtype.module'; import { AddonStorageManagerModule } from '@addon/storagemanager/storagemanager.module'; import { AddonFilterModule } from '@addon/filter/filter.module'; +import { AddonModH5PActivityModule } from '@addon/mod/h5pactivity/h5pactivity.module'; + +import { setSingletonsInjector } from '@singletons/core.singletons'; // For translate loader. AoT requires an exported function for factories. export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { @@ -237,6 +242,7 @@ export const WP_PROVIDER: any = null; CoreH5PModule, CoreSearchModule, CoreEditorModule, + CoreXAPIModule, AddonBadgesModule, AddonBlogModule, AddonCalendarModule, @@ -245,6 +251,7 @@ export const WP_PROVIDER: any = null; AddonUserProfileFieldModule, AddonFilesModule, AddonBlockActivityModulesModule, + AddonBlockActivityResultsModule, AddonBlockBadgesModule, AddonBlockBlogMenuModule, AddonBlockBlogRecentModule, @@ -299,7 +306,8 @@ export const WP_PROVIDER: any = null; AddonQbehaviourModule, AddonQtypeModule, AddonStorageManagerModule, - AddonFilterModule + AddonFilterModule, + AddonModH5PActivityModule, ], bootstrap: [IonicApp], entryComponents: [ @@ -357,6 +365,7 @@ export class AppModule { private eventsProvider: CoreEventsProvider, cronDelegate: CoreCronDelegate, siteInfoCronHandler: CoreSiteInfoCronHandler, + injector: Injector, ) { // Register a handler for platform ready. initDelegate.registerProcess({ @@ -391,6 +400,9 @@ export class AppModule { // Register handlers. cronDelegate.register(siteInfoCronHandler); + // Set the injector. + setSingletonsInjector(injector); + // Set transition animation. config.setTransition('core-page-transition', CorePageTransition); config.setTransition('core-modal-lateral-transition', CoreModalLateralTransition); diff --git a/src/app/app.scss b/src/app/app.scss index cc99bbfa8..51e512502 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -46,19 +46,6 @@ ion-app.app-root { text-transform: none; } - @include media-breakpoint-up(sm) { - .core-center-view .scroll-content { - display: flex!important; - align-content: center !important; - align-items: center !important; - > * { - margin: 0 auto; - width: 100%; - max-width: 600px; - } - } - } - @include media-breakpoint-down(sm) { .hidden-phone { display: none !important; @@ -111,6 +98,10 @@ ion-app.app-root { } } + .item[detail-push] { + cursor: pointer; + } + .core-nav-item-selected, .item.core-nav-item-selected { @include core-selected-item($core-splitview-selected); } @@ -450,6 +441,18 @@ ion-app.app-root { } } } + @include darkmode() { + @each $color-name, $color-base, $color-contrast in get-colors($colors-dark) { + &.select-md-#{$color-name}, + &.select-ios-#{$color-name} { + color: $color-base; + + .select-icon .select-icon-inner { + color: $color-base; + } + } + } + } } .item-label-stacked ion-select[multiple="true"] { @@ -666,11 +669,12 @@ ion-app.app-root { overflow: auto; } - .action-sheet-wrapper { + ion-action-sheet .action-sheet-wrapper .action-sheet-container { .action-sheet-button.action-sheet-cancel { color: $core-action-sheet-cancel-color; @include darkmode() { color: $core-dark-action-sheet-cancel-color; + background-color: $black; } } .action-sheet-selected { @@ -699,6 +703,16 @@ ion-app.app-root { border: 0; } + ion-alert .alert-message + div:not(.alert-button-group) { + overflow: auto; + alert-checkbox-group, + alert-radio-group, + alert-input-group { + overflow: visible; + max-height: none; + } + } + ion-toast.core-toast-success .toast-wrapper{ background: $green-dark; } @@ -1287,3 +1301,21 @@ ion-app.app-root { } } } + +// QR scan. The scanner is at the background of the app, we need to hide the elements that overlay it. +.core-scanning-qr { + ion-app.app-root { + background-color: transparent !important; + + .ion-page { + background-color: transparent !important; + } + ion-content, ion-backdrop, ion-modal:not(.core-modal-fullscreen), core-ion-tabs { + display: none !important; + } + + &.ios .ion-page.show-page~.nav-decor { + display: none !important; + } + } +} diff --git a/src/assets/img/icons/empty.svg b/src/assets/img/icons/empty.svg new file mode 100644 index 000000000..a16fb7460 --- /dev/null +++ b/src/assets/img/icons/empty.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/src/assets/img/login/faq_qrcode.png b/src/assets/img/login/faq_qrcode.png new file mode 100644 index 000000000..cc936b168 Binary files /dev/null and b/src/assets/img/login/faq_qrcode.png differ diff --git a/src/assets/img/mod/h5pactivity.svg b/src/assets/img/mod/h5pactivity.svg new file mode 100644 index 000000000..97fef5728 --- /dev/null +++ b/src/assets/img/mod/h5pactivity.svg @@ -0,0 +1 @@ +h5p finalArtboard 1 \ No newline at end of file diff --git a/src/assets/js/iframe-recaptcha.js b/src/assets/js/iframe-recaptcha.js new file mode 100644 index 000000000..f1ff5e843 --- /dev/null +++ b/src/assets/js/iframe-recaptcha.js @@ -0,0 +1,42 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +(function () { + var url = location.href; + + if (!url.match(/^https?:\/\//i) || !url.match(/\/webservice\/recaptcha\.php/i)) { + // Not the recaptcha script, stop. + return; + } + + // Define recaptcha callbacks. + window.recaptchacallback = function(value) { + window.parent.postMessage({ + environment: 'moodleapp', + context: 'recaptcha', + action: 'callback', + frameUrl: location.href, + value: value, + }, '*'); + }; + + window.recaptchaexpiredcallback = function() { + window.parent.postMessage({ + environment: 'moodleapp', + context: 'recaptcha', + action: 'expired', + frameUrl: location.href, + }, '*'); + }; +})(); \ No newline at end of file diff --git a/src/assets/js/iframe-treat-links.js b/src/assets/js/iframe-treat-links.js new file mode 100644 index 000000000..68f83571b --- /dev/null +++ b/src/assets/js/iframe-treat-links.js @@ -0,0 +1,210 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +(function () { + var url = location.href; + + if (url.match(/^moodleappfs:\/\/localhost/i) || !url.match(/^[a-z0-9]+:\/\//i)) { + // Same domain as the app, stop. + return; + } + + // Redefine window.open. + window.open = function(url, name, specs) { + if (name == '_self') { + // Link should be loaded in the same frame. + location.href = toAbsolute(url); + + return; + } + + getRootWindow(window).postMessage({ + environment: 'moodleapp', + context: 'iframe', + action: 'window_open', + frameUrl: location.href, + url: url, + name: name, + specs: specs, + }, '*'); + }; + + // Handle link clicks. + document.addEventListener('click', (event) => { + if (event.defaultPrevented) { + // Event already prevented by some other code. + return; + } + + // Find the link being clicked. + var el = event.target; + while (el && el.tagName !== 'A') { + el = el.parentElement; + } + + if (!el || el.treated) { + return; + } + + // Add click listener to the link, this way if the iframe has added a listener to the link it will be executed first. + el.treated = true; + el.addEventListener('click', function(event) { + linkClicked(el, event); + }); + }, { + capture: true // Use capture to fix this listener not called if the element clicked is too deep in the DOM. + }); + + + + /** + * Concatenate two paths, adding a slash between them if needed. + * + * @param leftPath Left path. + * @param rightPath Right path. + * @return Concatenated path. + */ + function concatenatePaths(leftPath, rightPath) { + if (!leftPath) { + return rightPath; + } else if (!rightPath) { + return leftPath; + } + + var lastCharLeft = leftPath.slice(-1); + var firstCharRight = rightPath.charAt(0); + + if (lastCharLeft === '/' && firstCharRight === '/') { + return leftPath + rightPath.substr(1); + } else if (lastCharLeft !== '/' && firstCharRight !== '/') { + return leftPath + '/' + rightPath; + } else { + return leftPath + rightPath; + } + } + + /** + * Get the root window. + * + * @param win Current window to check. + * @return Root window. + */ + function getRootWindow(win) { + if (win.parent === win) { + return win; + } + + return getRootWindow(win.parent); + } + + /** + * Get the scheme from a URL. + * + * @param url URL to treat. + * @return Scheme, undefined if no scheme found. + */ + function getUrlScheme(url) { + if (!url) { + return; + } + + var matches = url.match(/^([a-z][a-z0-9+\-.]*):/); + if (matches && matches[1]) { + return matches[1]; + } + } + + /** + * Check if a URL is absolute. + * + * @param url URL to treat. + * @return Whether it's absolute. + */ + function isAbsoluteUrl(url) { + return /^[^:]{2,}:\/\//i.test(url); + } + + /** + * Check whether a URL scheme belongs to a local file. + * + * @param scheme Scheme to check. + * @return Whether the scheme belongs to a local file. + */ + function isLocalFileUrlScheme(scheme) { + if (scheme) { + scheme = scheme.toLowerCase(); + } + + return scheme == 'cdvfile' || + scheme == 'file' || + scheme == 'filesystem' || + scheme == 'moodleappfs'; + } + + /** + * Handle a click on an anchor element. + * + * @param link Anchor element clicked. + * @param event Click event. + */ + function linkClicked(link, event) { + if (event.defaultPrevented) { + // Event already prevented by some other code. + return; + } + + var linkScheme = getUrlScheme(link.href); + var pageScheme = getUrlScheme(location.href); + var isTargetSelf = !link.target || link.target == '_self'; + + if (!link.href || linkScheme == 'javascript') { + // Links with no URL and Javascript links are ignored. + return; + } + + event.preventDefault(); + + if (isTargetSelf && (isLocalFileUrlScheme(linkScheme) || !isLocalFileUrlScheme(pageScheme))) { + // Link should be loaded in the same frame. Don't do it if link is online and frame is local. + location.href = toAbsolute(link.href); + + return; + } + + getRootWindow(window).postMessage({ + environment: 'moodleapp', + context: 'iframe', + action: 'link_clicked', + frameUrl: location.href, + link: {href: link.href, target: link.target}, + }, '*'); + } + + /** + * Convert a URL to an absolute URL if needed using the frame src. + * + * @param url URL to convert. + * @return Absolute URL. + */ + function toAbsolute(url) { + if (isAbsoluteUrl(url)) { + return url; + } + + // It's a relative URL, use the frame src to create the full URL. + var pathToDir = location.href.substring(0, location.href.lastIndexOf('/')); + + return concatenatePaths(pathToDir, url); + } +})(); \ No newline at end of file diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index d9af5a22e..5c62acd84 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -27,6 +27,7 @@ "addon.badges.version": "Version", "addon.badges.warnexpired": "(This badge has expired!)", "addon.block_activitymodules.pluginname": "Activities", + "addon.block_activityresults.pluginname": "Activity results", "addon.block_badges.pluginname": "Latest badges", "addon.block_blogmenu.pluginname": "Blog menu", "addon.block_blogrecent.pluginname": "Recent blog entries", @@ -48,6 +49,7 @@ "addon.block_myoverview.nocourses": "No courses", "addon.block_myoverview.past": "Past", "addon.block_myoverview.pluginname": "Course overview", + "addon.block_myoverview.shortname": "Short name", "addon.block_myoverview.title": "Course name", "addon.block_newsitems.pluginname": "Latest announcements", "addon.block_onlineusers.pluginname": "Online users", @@ -657,6 +659,40 @@ "addon.mod_glossary.noentriesfound": "No entries were found.", "addon.mod_glossary.searchquery": "Search query", "addon.mod_glossary.tagarea_glossary_entries": "Glossary entries", + "addon.mod_h5pactivity.all_attempts": "All user attempts", + "addon.mod_h5pactivity.answer_checked": "Answer checked", + "addon.mod_h5pactivity.answer_correct": "Your answer is correct", + "addon.mod_h5pactivity.answer_fail": "Incorrect answer", + "addon.mod_h5pactivity.answer_incorrect": "Your answer is incorrect", + "addon.mod_h5pactivity.answer_pass": "Correct answer", + "addon.mod_h5pactivity.attempt": "Attempt", + "addon.mod_h5pactivity.attempt_completion_no": "This attempt is not marked as completed", + "addon.mod_h5pactivity.attempt_completion_yes": "This attempt is completed", + "addon.mod_h5pactivity.attempt_success_fail": "Fail", + "addon.mod_h5pactivity.attempt_success_pass": "Pass", + "addon.mod_h5pactivity.attempt_success_unknown": "Not reported", + "addon.mod_h5pactivity.attempts_none": "This user has no attempts to display.", + "addon.mod_h5pactivity.completion": "Completion", + "addon.mod_h5pactivity.downloadh5pfile": "Download H5P file", + "addon.mod_h5pactivity.duration": "Duration", + "addon.mod_h5pactivity.errorgetactivity": "Error getting H5P activity data.", + "addon.mod_h5pactivity.filestatenotdownloaded": "The H5P package is not downloaded. You need to download it to be able to use it.", + "addon.mod_h5pactivity.filestateoutdated": "The H5P package has been modified since the last download. You need to download it again to be able to use it.", + "addon.mod_h5pactivity.maxscore": "Max score", + "addon.mod_h5pactivity.modulenameplural": "H5P", + "addon.mod_h5pactivity.myattempts": "My attempts", + "addon.mod_h5pactivity.no_compatible_track": "This interaction ({{$a}}) does not provide tracking information or the tracking provided is not compatible with the current activity version.", + "addon.mod_h5pactivity.offlinedisabledwarning": "You need to be online to view the H5P package.", + "addon.mod_h5pactivity.outcome": "Outcome", + "addon.mod_h5pactivity.previewmode": "This content is displayed in preview mode. No attempt tracking will be stored.", + "addon.mod_h5pactivity.result_fill-in": "Fill-in text", + "addon.mod_h5pactivity.result_other": "Unkown interaction type", + "addon.mod_h5pactivity.review_my_attempts": "View my attempts", + "addon.mod_h5pactivity.score": "Score", + "addon.mod_h5pactivity.score_out_of": "{{$a.rawscore}} out of {{$a.maxscore}}", + "addon.mod_h5pactivity.startdate": "Start date", + "addon.mod_h5pactivity.totalscore": "Total score", + "addon.mod_h5pactivity.viewattempt": "View attempt {{$a}}", "addon.mod_imscp.deploymenterror": "Content package error!", "addon.mod_imscp.modulenameplural": "IMS content packages", "addon.mod_imscp.showmoduledescription": "Show description", @@ -870,12 +906,10 @@ "addon.mod_scorm.highestattempt": "Highest attempt", "addon.mod_scorm.incomplete": "Incomplete", "addon.mod_scorm.lastattempt": "Last completed attempt", - "addon.mod_scorm.mode": "Mode", "addon.mod_scorm.modulenameplural": "SCORM packages", "addon.mod_scorm.newattempt": "Start a new attempt", "addon.mod_scorm.noattemptsallowed": "Number of attempts allowed", "addon.mod_scorm.noattemptsmade": "Number of attempts you have made", - "addon.mod_scorm.normal": "Normal", "addon.mod_scorm.notattempted": "Not attempted", "addon.mod_scorm.offlineattemptnote": "This attempt has data that hasn't been synchronised.", "addon.mod_scorm.offlineattemptovermax": "This attempt cannot be sent because you exceeded the maximum number of attempts.", @@ -1012,6 +1046,7 @@ "addon.notifications.playsound": "Play sound", "addon.notifications.therearentnotificationsyet": "There are no notifications.", "addon.storagemanager.deletecourse": "Offload all course data", + "addon.storagemanager.deletecourses": "Offload all courses data", "addon.storagemanager.deletedatafrom": "Offload data from {{name}}", "addon.storagemanager.info": "Files stored on your device make the app work faster and enable the app to be used offline. You can safely offload files if you need to free up storage space.", "addon.storagemanager.managestorage": "Manage storage", @@ -1331,7 +1366,9 @@ "core.block.blocks": "Blocks", "core.browser": "Browser", "core.cancel": "Cancel", - "core.cannotconnect": "Cannot connect: Verify that you have correctly typed your site address.", + "core.cannotconnect": "Cannot connect", + "core.cannotconnecttrouble": "We're having trouble connecting to your site.", + "core.cannotconnectverify": "Please check the address is correct.", "core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.", "core.captureaudio": "Record audio", "core.capturedimage": "Taken picture.", @@ -1370,6 +1407,7 @@ "core.confirmdeletefile": "Are you sure you want to delete this file?", "core.confirmgotabroot": "Are you sure you want to go back to {{name}}?", "core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?", + "core.confirmleaveunknownchanges": "Are you sure you want to leave this page? If you have unsaved changes they will be lost.", "core.confirmloss": "Are you sure? All changes will be lost.", "core.confirmopeninbrowser": "Do you want to open it in a web browser?", "core.considereddigitalminor": "You are too young to create an account on this site.", @@ -1383,6 +1421,7 @@ "core.contentlinks.errorredirectothersite": "The redirect URL cannot point to a different site.", "core.continue": "Continue", "core.copiedtoclipboard": "Text copied to clipboard", + "core.copytoclipboard": "Copy to clipboard", "core.course": "Course", "core.course.activitydisabled": "Your organisation has disabled this activity in the mobile app.", "core.course.activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.", @@ -1390,6 +1429,7 @@ "core.course.allsections": "All sections", "core.course.askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.", "core.course.availablespace": " You currently have about {{available}} free space.", + "core.course.cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", "core.course.confirmdeletemodulefiles": "Are you sure you want to delete these files?", "core.course.confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", "core.course.confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?", @@ -1611,6 +1651,7 @@ "core.h5p.editor": "Editor", "core.h5p.embed": "Embed", "core.h5p.embedtitle": "View the embed code for this content.", + "core.h5p.errorgetemail": "Error obtaining the user email. Please check your connection and try again.", "core.h5p.fullscreen": "Fullscreen", "core.h5p.gpl": "General Public License v3", "core.h5p.h5ptitle": "Visit h5p.org to check out more content.", @@ -1707,7 +1748,21 @@ "core.login.emailnotmatch": "Emails do not match", "core.login.erroraccesscontrolalloworigin": "The cross-origin call you're trying to perform has been rejected. Please check https://docs.moodle.org/dev/Moodle_Mobile_development_using_Chrome_or_Chromium", "core.login.errordeletesite": "An error occurred while deleting this site. Please try again.", + "core.login.errorexampleurl": "The URL https://campus.example.edu is only an example URL, it's not a real site. Please use the URL of your school or organization's site.", "core.login.errorupdatesite": "An error occurred while updating the site's token.", + "core.login.faqcannotconnectanswer": "Please, contact your site administrator.", + "core.login.faqcannotconnectquestion": "I typed my site address correctly but I still can't connect.", + "core.login.faqcannotfindmysiteanswer": "Have you typed the name correctly? It's also possible that your site is not included in our public sites directory. If you still can't find it, please enter your site address instead.", + "core.login.faqcannotfindmysitequestion": "I can't find my site.", + "core.login.faqsetupsiteanswer": "Visit {{$link}} to check out the different options you have to create your own Moodle site.", + "core.login.faqsetupsitelinktitle": "Get started.", + "core.login.faqsetupsitequestion": "I want to set up my own Moodle site.", + "core.login.faqtestappanswer": "To test the app in a Moodle Demo Site, type \"teacher\" or \"student\" in the \"Your site\" field and click the \"Connect to your site\" button.", + "core.login.faqtestappquestion": "I just want to test the app, what can I do?", + "core.login.faqwhatisurlanswer": "

Every organisation has their own unique address or URL for their Moodle site. To find the address:

  1. Open a web browser and go to your Moodle site login page.
  2. At the top of the page, in the address bar, you will see the URL of your Moodle site e.g. \"campus.example.edu\".
    {{$image}}
  3. Copy the address (do not copy the /login and what comes after), paste it into the Moodle app then click \"Connect to your site\"
  4. Now you can log in to your site using your username and password.
  5. ", + "core.login.faqwhatisurlquestion": "What is my site address? How can I find my site URL?", + "core.login.faqwhereisqrcode": "Where can I find the QR code?", + "core.login.faqwhereisqrcodeanswer": "

    If your organisation has enabled it, you will find a QR code on the web site at the bottom of your user profile page.

    {{$image}}", "core.login.findyoursite": "Find your site", "core.login.firsttime": "Is this your first time here?", "core.login.forcepasswordchangenotice": "You must change your password to proceed.", @@ -1737,6 +1792,17 @@ "core.login.mustconfirm": "You need to confirm your account", "core.login.newaccount": "New account", "core.login.notloggedin": "You need to be logged in.", + "core.login.onboardingcreatemanagecourses": "Create & manage your courses", + "core.login.onboardingenrolmanagestudents": "Enrol & manage your students", + "core.login.onboardinggetstarted": "Get started with Moodle", + "core.login.onboardingialreadyhaveasite": "I already have a Moodle site", + "core.login.onboardingimalearner": "I'm a learner", + "core.login.onboardingimaneducator": "I'm an educator", + "core.login.onboardingineedasite": "I need a Moodle site", + "core.login.onboardingprovidefeedback": "Provide timely feedback", + "core.login.onboardingtoconnect": "To connect to the Moodle App you'll need a Moodle site", + "core.login.onboardingwelcome": "Welcome to the Moodle App!", + "core.login.or": "OR", "core.login.password": "Password", "core.login.passwordforgotten": "Forgotten password", "core.login.passwordforgotteninstructions2": "To reset your password, submit your username or your email address below. If we can find you in the database, an email will be sent to your email address, with instructions how to get access again.", @@ -1746,8 +1812,6 @@ "core.login.policyagreement": "Site policy agreement", "core.login.policyagreementclick": "Link to site policy agreement", "core.login.potentialidps": "Log in using your account on:", - "core.login.problemconnectingerror": "We're having trouble connecting to", - "core.login.problemconnectingerrorcontinue": "Double check you've entered the address correctly and try again.", "core.login.profileinvaliddata": "Invalid value", "core.login.recaptchachallengeimage": "reCAPTCHA challenge image", "core.login.recaptchaexpired": "Verification expired. Answer the security question again.", @@ -1761,7 +1825,7 @@ "core.login.selectacountry": "Select a country", "core.login.selectsite": "Please select your site:", "core.login.signupplugindisabled": "{{$a}} is not enabled.", - "core.login.siteaddress": "Your site address", + "core.login.siteaddress": "Your site", "core.login.sitehasredirect": "Your site contains at least one HTTP redirect. The app cannot follow redirects, this could be the issue that's preventing the app from connecting to your site.", "core.login.siteinmaintenance": "Your site is in maintenance mode", "core.login.sitepolicynotagreederror": "Site policy not agreed.", @@ -1775,7 +1839,9 @@ "core.login.usernamerequired": "Username required", "core.login.usernotaddederror": "User not added - error", "core.login.visitchangepassword": "Do you want to visit the site to change the password?", - "core.login.webservicesnotenabled": "Web services are not enabled in your site. Please contact your site administrator if you think they should be enabled.", + "core.login.webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.", + "core.login.youcanstillconnectwithcredentials": "You can still connect to the site by entering your username and password.", + "core.login.yourenteredsite": "Connect to your site", "core.lostconnection": "Your authentication token is invalid or has expired. You will have to reconnect to the site.", "core.mainmenu.changesite": "Change site", "core.mainmenu.help": "Help", @@ -1798,6 +1864,7 @@ "core.mod_folder": "Folder", "core.mod_forum": "Forum", "core.mod_glossary": "Glossary", + "core.mod_h5pactivity": "H5P", "core.mod_ims": "IMS content package", "core.mod_imscp": "IMS content package", "core.mod_label": "Label", @@ -1815,6 +1882,7 @@ "core.more": "more", "core.mygroups": "My groups", "core.name": "Name", + "core.needhelp": "Need help?", "core.networkerroriframemsg": "This content is not available offline. Please connect to the internet and try again.", "core.networkerrormsg": "There was a problem connecting to the site. Please check your connection and try again.", "core.never": "Never", @@ -1842,6 +1910,7 @@ "core.online": "Online", "core.openfullimage": "Click here to display the full size image", "core.openinbrowser": "Open in browser", + "core.openmodinbrowser": "Open {{$a}} in browser", "core.othergroups": "Other groups", "core.pagea": "Page {{$a}}", "core.parentlanguage": "", @@ -1852,6 +1921,7 @@ "core.previous": "Previous", "core.proceed": "Proceed", "core.pulltorefresh": "Pull to refresh", + "core.qrscanner": "QR scanner", "core.question.answer": "Answer", "core.question.answersaved": "Answer saved", "core.question.cannotdeterminestatus": "Cannot determine status", @@ -1894,6 +1964,7 @@ "core.retry": "Retry", "core.save": "Save", "core.savechanges": "Save changes", + "core.scanqr": "Scan QR code", "core.search": "Search", "core.searching": "Searching", "core.searchresults": "Search results", @@ -1997,10 +2068,12 @@ "core.sizekb": "KB", "core.sizemb": "MB", "core.sizetb": "TB", + "core.skip": "Skip", "core.sorry": "Sorry...", "core.sort": "Sort", "core.sortby": "Sort by", "core.start": "Start", + "core.storingfiles": "Storing files", "core.strftimedate": "%d %B %Y", "core.strftimedatefullshort": "%d/%m/%y", "core.strftimedateshort": "%d %B", @@ -2089,6 +2162,7 @@ "core.warningofflinedatadeleted": "Offline data from {{component}} '{{name}}' has been deleted. {{error}}", "core.whatisyourage": "What is your age?", "core.wheredoyoulive": "In which country do you live?", + "core.whoissiteadmin": "\"Site Administrators\" are the people who manage the Moodle at your school/university/company or learning organisation. If you don't know how to contact them, please contact your teachers/trainers.", "core.whoops": "Oops!", "core.whyisthishappening": "Why is this happening?", "core.whyisthisrequired": "Why is this required?", diff --git a/src/classes/native-to-angular-http.ts b/src/classes/native-to-angular-http.ts new file mode 100644 index 000000000..e21e7eb7e --- /dev/null +++ b/src/classes/native-to-angular-http.ts @@ -0,0 +1,98 @@ +// (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 { HttpResponse as AngularHttpResponse, HttpHeaders } from '@angular/common/http'; +import { HTTPResponse as NativeHttpResponse } from '@ionic-native/http'; + +const HTTP_STATUS_MESSAGES = { + 100: 'Continue', + 101: 'Switching Protocol', + 102: 'Processing', + 103: 'Early Hints', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 208: 'Already Reported', + 226: 'IM Used', + 300: 'Multiple Choice', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 306: 'unused', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Payload Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: 'I\'m a teapot', + 421: 'Misdirected Request', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 510: 'Not Extended', + 511: 'Network Authentication Required', +}; + +/** + * Class that adapts a Cordova plugin http response to an Angular http response. + */ +export class CoreNativeToAngularHttpResponse extends AngularHttpResponse { + + constructor(protected nativeResponse: NativeHttpResponse) { + super({ + body: nativeResponse.data, + headers: new HttpHeaders(nativeResponse.headers), + status: nativeResponse.status, + statusText: HTTP_STATUS_MESSAGES[nativeResponse.status] || '', + url: nativeResponse.url || '' + }); + } +} diff --git a/src/classes/singletons-factory.ts b/src/classes/singletons-factory.ts new file mode 100644 index 000000000..791b192f0 --- /dev/null +++ b/src/classes/singletons-factory.ts @@ -0,0 +1,76 @@ +// (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 { Injector, Type } from '@angular/core'; + +/** + * Stub class used to type anonymous classes created in CoreSingletonsFactory#makeSingleton method. + */ +class CoreSingleton {} + +/** + * Token that can be used to resolve instances from the injector. + */ +export type CoreInjectionToken = Type | Type | string; + +/** + * Singleton class created using the factory. + */ +export type CoreSingletonClass = typeof CoreSingleton & { instance: Service }; + +/** + * Factory used to create CoreSingleton classes that get instances from an injector. + */ +export class CoreSingletonsFactory { + + /** + * Angular injector used to resolve singleton instances. + */ + private injector: Injector; + + /** + * Set the injector that will be used to resolve instances in the singletons created with this factory. + * + * @param injector Injector. + */ + setInjector(injector: Injector): void { + this.injector = injector; + } + + /** + * Make a singleton that will hold an instance resolved from the factory injector. + * + * @param injectionToken Injection token used to resolve the singleton instance. This is usually the service class if the + * provider was defined using a class or the string used in the `provide` key if it was defined using an object. + */ + makeSingleton(injectionToken: CoreInjectionToken): CoreSingletonClass { + // tslint:disable: no-this-assignment + const factory = this; + + return class { + + private static _instance: Service; + + static get instance(): Service { + // Initialize instances lazily. + if (!this._instance) { + this._instance = factory.injector.get(injectionToken); + } + + return this._instance; + } + + }; + } +} diff --git a/src/classes/site.ts b/src/classes/site.ts index 54b2dd294..463b5a027 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -14,7 +14,6 @@ import { Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { HttpClient } from '@angular/common/http'; import { SQLiteDB } from './sqlitedb'; import { CoreAppProvider } from '@providers/app'; import { CoreDbProvider } from '@providers/db'; @@ -190,7 +189,6 @@ export class CoreSite { protected domUtils: CoreDomUtilsProvider; protected eventsProvider: CoreEventsProvider; protected fileProvider: CoreFileProvider; - protected http: HttpClient; protected textUtils: CoreTextUtilsProvider; protected timeUtils: CoreTimeUtilsProvider; protected translate: TranslateService; @@ -256,7 +254,6 @@ export class CoreSite { this.domUtils = injector.get(CoreDomUtilsProvider); this.eventsProvider = injector.get(CoreEventsProvider); this.fileProvider = injector.get(CoreFileProvider); - this.http = injector.get(HttpClient); this.textUtils = injector.get(CoreTextUtilsProvider); this.timeUtils = injector.get(CoreTimeUtilsProvider); this.translate = injector.get(TranslateService); @@ -1357,55 +1354,67 @@ export class CoreSite { * @param retrying True if we're retrying the check. * @return Promise resolved when the check is done. */ - checkLocalMobilePlugin(retrying?: boolean): Promise { + async checkLocalMobilePlugin(retrying?: boolean): Promise { const checkUrl = this.siteUrl + '/local/mobile/check.php', service = CoreConfigConstants.wsextservice; if (!service) { // External service not defined. - return Promise.resolve({ code: 0 }); + return { code: 0 }; } - const promise = this.http.post(checkUrl, { service: service }).timeout(this.wsProvider.getRequestTimeout()).toPromise(); + let data; - return promise.then((data: any) => { - if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') { - if (!retrying) { - this.siteUrl = this.urlUtils.addOrRemoveWWW(this.siteUrl); + try { + const response = await this.wsProvider.sendHTTPRequest(checkUrl, { + method: 'post', + data: { service: service }, + }); - return this.checkLocalMobilePlugin(true); - } else { - return Promise.reject(data.error); - } - } else if (typeof data == 'undefined' || typeof data.code == 'undefined') { - // The local_mobile returned something we didn't expect. Let's assume it's not installed. - return { code: 0, warning: 'core.login.localmobileunexpectedresponse' }; - } - - const code = parseInt(data.code, 10); - if (data.error) { - switch (code) { - case 1: - // Site in maintenance mode. - return Promise.reject(this.translate.instant('core.login.siteinmaintenance')); - case 2: - // Web services not enabled. - return Promise.reject(this.translate.instant('core.login.webservicesnotenabled')); - case 3: - // Extended service not enabled, but the official is enabled. - return { code: 0 }; - case 4: - // Neither extended or official services enabled. - return Promise.reject(this.translate.instant('core.login.mobileservicesnotenabled')); - default: - return Promise.reject(this.translate.instant('core.unexpectederror')); - } - } else { - return { code: code, service: service, coreSupported: !!data.coresupported }; - } - }, () => { + data = response.body; + } catch (ex) { return { code: 0 }; - }); + } + + if (data === null) { + // This probably means that the server was configured to return null for non-existing URLs. Not installed. + return { code: 0 }; + } + + if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') { + if (!retrying) { + this.siteUrl = this.urlUtils.addOrRemoveWWW(this.siteUrl); + + return this.checkLocalMobilePlugin(true); + } else { + throw data.error; + } + } else if (typeof data == 'undefined' || typeof data.code == 'undefined') { + // The local_mobile returned something we didn't expect. Let's assume it's not installed. + return { code: 0, warning: 'core.login.localmobileunexpectedresponse' }; + } + + const code = parseInt(data.code, 10); + if (data.error) { + switch (code) { + case 1: + // Site in maintenance mode. + throw this.translate.instant('core.login.siteinmaintenance'); + case 2: + // Web services not enabled. + throw this.translate.instant('core.login.webservicesnotenabled'); + case 3: + // Extended service not enabled, but the official is enabled. + return { code: 0 }; + case 4: + // Neither extended or official services enabled. + throw this.translate.instant('core.login.mobileservicesnotenabled'); + default: + throw this.translate.instant('core.unexpectederror'); + } + } else { + return { code: code, service: service, coreSupported: !!data.coresupported }; + } } /** @@ -1811,7 +1820,7 @@ export class CoreSite { this.lastAutoLogin = this.timeUtils.timestamp(); - return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + url; + return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + encodeURIComponent(url); }).catch(() => { // Couldn't get autologin key, return the same URL. @@ -1970,7 +1979,7 @@ export class CoreSite { url = this.fixPluginfileURL(url); this.tokenPluginFileWorksPromise = this.wsProvider.performHead(url).then((result) => { - return result.ok; + return result.status >= 200 && result.status < 300; }).catch((error) => { // Error performing head request. return false; diff --git a/src/components/empty-box/core-empty-box.html b/src/components/empty-box/core-empty-box.html index f131769f5..09a592094 100644 --- a/src/components/empty-box/core-empty-box.html +++ b/src/components/empty-box/core-empty-box.html @@ -1,7 +1,7 @@
    - +

    {{ message }}

    diff --git a/src/components/empty-box/empty-box.ts b/src/components/empty-box/empty-box.ts index 5c693c84c..e9480d0de 100644 --- a/src/components/empty-box/empty-box.ts +++ b/src/components/empty-box/empty-box.ts @@ -32,6 +32,7 @@ export class CoreEmptyBoxComponent { @Input() image?: string; // Image source. If an icon is provided, image won't be used. @Input() inline?: boolean; // If this has to be shown inline instead of occupying whole page. // If image or icon is not supplied, it's true by default. + @Input() flipIconRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false. constructor() { // Nothing to do. diff --git a/src/components/file/file.ts b/src/components/file/file.ts index 22534fe16..5df003e25 100644 --- a/src/components/file/file.ts +++ b/src/components/file/file.ts @@ -162,10 +162,10 @@ export class CoreFileComponent implements OnInit, OnDestroy { // Local file. this.utils.openFile(this.file.toURL()); } else if (this.fileUrl) { - if (this.fileUrl.indexOf('http') === 0) { - this.utils.openOnlineFile(this.urlUtils.unfixPluginfileURL(this.fileUrl)); - } else { + if (this.urlUtils.isLocalFileUrl(this.fileUrl)) { this.utils.openFile(this.fileUrl); + } else { + this.utils.openOnlineFile(this.urlUtils.unfixPluginfileURL(this.fileUrl)); } } diff --git a/src/components/icon/icon.scss b/src/components/icon/icon.scss index a80559c8e..f14ba848a 100644 --- a/src/components/icon/icon.scss +++ b/src/components/icon/icon.scss @@ -7,7 +7,9 @@ @each $color-name, $color-base, $color-contrast in get-colors($colors-dark) { .fa-#{$color-name} { - color: $color-base !important; + @include darkmode() { + color: $color-base !important; + } } } diff --git a/src/components/icon/icon.ts b/src/components/icon/icon.ts index 9bab34f9b..72ac16416 100644 --- a/src/components/icon/icon.ts +++ b/src/components/icon/icon.ts @@ -40,6 +40,7 @@ export class CoreIconComponent implements OnChanges, OnDestroy { @Input('fixed-width') fixedWidth: string; @Input('label') ariaLabel?: string; + @Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false. protected element: HTMLElement; protected newElement: HTMLElement; @@ -106,6 +107,10 @@ export class CoreIconComponent implements OnChanges, OnDestroy { this.newElement.classList.add('icon-slash'); } + if (this.flipRtl) { + this.newElement.classList.add('core-icon-dir-flip'); + } + oldElement.parentElement.replaceChild(this.newElement, oldElement); } diff --git a/src/components/iframe/core-iframe.html b/src/components/iframe/core-iframe.html index 9e5f98ae6..0ba267b63 100644 --- a/src/components/iframe/core-iframe.html +++ b/src/components/iframe/core-iframe.html @@ -1,5 +1,7 @@ -
    - +
    + + + diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index 516012a8f..14dc98c54 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -13,22 +13,25 @@ // limitations under the License. import { - Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Optional + Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -import { NavController } from 'ionic-angular'; +import { NavController, Platform } from 'ionic-angular'; +import { CoreFile } from '@providers/file'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; import { CoreIframeUtilsProvider } from '@providers/utils/iframe'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreSplitViewComponent } from '@components/split-view/split-view'; +import { CoreUrl } from '@singletons/url'; +import { WKWebViewCookiesWindow } from 'cordova-plugin-wkwebview-cookies'; @Component({ selector: 'core-iframe', templateUrl: 'core-iframe.html' }) -export class CoreIframeComponent implements OnInit, OnChanges { +export class CoreIframeComponent implements OnChanges { @ViewChild('iframe') iframe: ElementRef; @Input() src: string; @@ -41,6 +44,7 @@ export class CoreIframeComponent implements OnInit, OnChanges { protected logger; protected IFRAME_TIMEOUT = 15000; + protected initialized = false; constructor(logger: CoreLoggerProvider, protected iframeUtils: CoreIframeUtilsProvider, @@ -49,16 +53,23 @@ export class CoreIframeComponent implements OnInit, OnChanges { protected navCtrl: NavController, protected urlUtils: CoreUrlUtilsProvider, protected utils: CoreUtilsProvider, - @Optional() protected svComponent: CoreSplitViewComponent) { + @Optional() protected svComponent: CoreSplitViewComponent, + protected platform: Platform) { this.logger = logger.getInstance('CoreIframe'); this.loaded = new EventEmitter(); } /** - * Component being initialized. + * Init the data. */ - ngOnInit(): void { + protected init(): void { + if (this.initialized) { + return; + } + + this.initialized = true; + const iframe: HTMLIFrameElement = this.iframe && this.iframe.nativeElement; this.iframeWidth = this.domUtils.formatPixelsSize(this.iframeWidth) || '100%'; @@ -66,22 +77,22 @@ export class CoreIframeComponent implements OnInit, OnChanges { this.allowFullscreen = this.utils.isTrueOrOne(this.allowFullscreen); // Show loading only with external URLs. - this.loading = !this.src || !!this.src.match(/^https?:\/\//i); + this.loading = !this.src || !this.urlUtils.isLocalFileUrl(this.src); const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; this.iframeUtils.treatFrame(iframe, false, navCtrl); + iframe.addEventListener('load', () => { + this.loading = false; + this.loaded.emit(iframe); // Notify iframe was loaded. + }); + + iframe.addEventListener('error', () => { + this.loading = false; + this.domUtils.showErrorModal('core.errorloadingcontent', true); + }); + if (this.loading) { - iframe.addEventListener('load', () => { - this.loading = false; - this.loaded.emit(iframe); // Notify iframe was loaded. - }); - - iframe.addEventListener('error', () => { - this.loading = false; - this.domUtils.showErrorModal('core.errorloadingcontent', true); - }); - setTimeout(() => { this.loading = false; }, this.IFRAME_TIMEOUT); @@ -91,10 +102,35 @@ export class CoreIframeComponent implements OnInit, OnChanges { /** * Detect changes on input properties. */ - ngOnChanges(changes: {[name: string]: SimpleChange }): void { + async ngOnChanges(changes: {[name: string]: SimpleChange }): Promise { if (changes.src) { - const youtubeUrl = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue); - this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(youtubeUrl || changes.src.currentValue); + const url = this.urlUtils.getYoutubeEmbedUrl(changes.src.currentValue) || changes.src.currentValue; + + if (this.platform.is('ios') && url && !this.urlUtils.isLocalFileUrl(url)) { + // Save a "fake" cookie for the iframe's domain to fix a bug in WKWebView. + try { + const win = window; + const urlParts = CoreUrl.parse(url); + + if (urlParts.domain) { + await win.WKWebViewCookies.setCookie({ + name: 'MoodleAppCookieForWKWebView', + value: '1', + domain: urlParts.domain, + }); + } + } catch (err) { + // Ignore errors. + this.logger.error('Error setting cookie', err); + } + } + + this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(CoreFile.instance.convertFileSrc(url)); + + // Now that the URL has been set, initialize the iframe. Wait for the iframe to the added to the DOM. + setTimeout(() => { + this.init(); + }); } } } diff --git a/src/components/ion-tabs/ion-tabs.scss b/src/components/ion-tabs/ion-tabs.scss index 523e1dd53..e64b20c00 100644 --- a/src/components/ion-tabs/ion-tabs.scss +++ b/src/components/ion-tabs/ion-tabs.scss @@ -44,7 +44,7 @@ ion-app.app-root core-ion-tabs { } } - .tabbar[hidden] + .tabcontent { + .tabbar[hidden] + .tabcontent, &.tabbar-hidden .tabcontent { width: 100%; core-ion-tab { @include position(0, 0, 0, 0); diff --git a/src/components/ion-tabs/ion-tabs.ts b/src/components/ion-tabs/ion-tabs.ts index da3c1e4f2..bd6c6afab 100644 --- a/src/components/ion-tabs/ion-tabs.ts +++ b/src/components/ion-tabs/ion-tabs.ts @@ -388,4 +388,11 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy { } } } + + /** + * @inheritdoc + */ + setTabbarHidden(tabbarHidden: boolean): void { + // Don't hide the tab bar, we'll do it via CSS if needed. + } } diff --git a/src/components/local-file/local-file.ts b/src/components/local-file/local-file.ts index c9f63ed17..cd0c573a3 100644 --- a/src/components/local-file/local-file.ts +++ b/src/components/local-file/local-file.ts @@ -78,7 +78,7 @@ export class CoreLocalFileComponent implements OnInit { this.size = this.textUtils.bytesToSize(metadata.size, 2); } - this.timemodified = this.timeUtils.userDate(metadata.modificationTime, 'core.strftimedatetimeshort'); + this.timemodified = this.timeUtils.userDate(metadata.modificationTime.getTime(), 'core.strftimedatetimeshort'); }); } diff --git a/src/components/navigation-bar/navigation-bar.ts b/src/components/navigation-bar/navigation-bar.ts index c19aad5ac..5eb878cb2 100644 --- a/src/components/navigation-bar/navigation-bar.ts +++ b/src/components/navigation-bar/navigation-bar.ts @@ -47,7 +47,13 @@ export class CoreNavigationBarComponent { } showInfo(): void { - this.textUtils.expandText(this.title, this.info, this.component, this.componentId, [], true, this.contextLevel, - this.contextInstanceId, this.courseId); + this.textUtils.viewText(this.title, this.info, { + component: this.component, + componentId: this.componentId, + filter: true, + contextLevel: this.contextLevel, + instanceId: this.contextInstanceId, + courseId: this.courseId, + }); } } diff --git a/src/components/recaptcha/recaptchamodal.ts b/src/components/recaptcha/recaptchamodal.ts index 1280e569f..2cff04c18 100644 --- a/src/components/recaptcha/recaptchamodal.ts +++ b/src/components/recaptcha/recaptchamodal.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; +import { Component, OnDestroy } from '@angular/core'; import { ViewController, NavParams } from 'ionic-angular'; /** @@ -22,13 +22,19 @@ import { ViewController, NavParams } from 'ionic-angular'; selector: 'core-recaptcha-modal', templateUrl: 'core-recaptchamodal.html' }) -export class CoreRecaptchaModalComponent { +export class CoreRecaptchaModalComponent implements OnDestroy { expired = false; value = ''; src: string; + protected messageListenerFunction: (event: MessageEvent) => Promise; + constructor(protected viewCtrl: ViewController, params: NavParams) { this.src = params.get('src'); + + // Listen for messages from the iframe. + this.messageListenerFunction = this.onIframeMessage.bind(this); + window.addEventListener('message', this.messageListenerFunction); } /** @@ -51,18 +57,63 @@ export class CoreRecaptchaModalComponent { const contentWindow = iframe && iframe.contentWindow; if (contentWindow) { - // Set the callbacks we're interested in. - contentWindow['recaptchacallback'] = (value): void => { - this.expired = false; - this.value = value; - this.closeModal(); - }; - - contentWindow['recaptchaexpiredcallback'] = (): void => { - // Verification expired. Check the checkbox again. - this.expired = true; - this.value = ''; - }; + try { + // Set the callbacks we're interested in. + contentWindow['recaptchacallback'] = this.onRecaptchaCallback.bind(this); + contentWindow['recaptchaexpiredcallback'] = this.onRecaptchaExpiredCallback.bind(this); + } catch (error) { + // Cannot access the window. + } } } + + /** + * Treat an iframe message event. + * + * @param event Event. + * @return Promise resolved when done. + */ + protected async onIframeMessage(event: MessageEvent): Promise { + if (!event.data || event.data.environment != 'moodleapp' || event.data.context != 'recaptcha') { + return; + } + + switch (event.data.action) { + case 'callback': + this.onRecaptchaCallback(event.data.value); + break; + case 'expired': + this.onRecaptchaExpiredCallback(); + break; + + default: + break; + } + } + + /** + * Recapcha callback called. + * + * @param value Value received. + */ + protected onRecaptchaCallback(value: any): void { + this.expired = false; + this.value = value; + this.closeModal(); + } + + /** + * Recapcha expired callback called. + */ + protected onRecaptchaExpiredCallback(): void { + this.expired = true; + this.value = ''; + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + window.removeEventListener('message', this.messageListenerFunction); + } } diff --git a/src/components/split-view/placeholder/core-placeholder.html b/src/components/split-view/placeholder/core-placeholder.html index e62e3e7b1..83991ff49 100644 --- a/src/components/split-view/placeholder/core-placeholder.html +++ b/src/components/split-view/placeholder/core-placeholder.html @@ -5,5 +5,5 @@ - + diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts index ed0910a49..65cdb3bf2 100644 --- a/src/components/tabs/tabs.ts +++ b/src/components/tabs/tabs.ts @@ -477,6 +477,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe * @param scrollElement Scroll element to check scroll position. */ showHideTabs(scrollElement: any): void { + if (!this.tabBarHeight && this.topTabsElement.offsetHeight != this.tabBarHeight) { + // Wrong tab height, recalculate it. + this.calculateTabBarHeight(); + } + if (!this.tabBarHeight) { // We don't have the tab bar height, this means the tab bar isn't shown. return; diff --git a/src/config.json b/src/config.json index 50897768a..9ac712c80 100644 --- a/src/config.json +++ b/src/config.json @@ -2,8 +2,8 @@ "app_id": "com.moodle.moodlemobile", "appname": "Moodle Mobile", "desktopappname": "Moodle Desktop", - "versioncode": 3820, - "versionname": "3.8.2", + "versioncode": 3900, + "versionname": "3.9.0", "cache_update_frequency_usually": 420000, "cache_update_frequency_often": 1200000, "cache_update_frequency_sometimes": 3600000, @@ -61,12 +61,12 @@ "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" } @@ -82,7 +82,7 @@ "multisitesdisplay": "", "skipssoconfirmation": false, "forcedefaultlanguage": false, - "privacypolicy": "https:\/\/moodle.net\/moodle-app-privacy\/", + "privacypolicy": "https://moodle.net/moodle-app-privacy/", "notificoncolor": "#f98012", "statusbarbg": false, "statusbarlighttext": false, @@ -93,5 +93,14 @@ "statusbarbgremotetheme": "#000000", "statusbarlighttextremotetheme": true, "enableanalytics": false, - "forceColorScheme": "" -} \ No newline at end of file + "enableonboarding": true, + "forceColorScheme": "", + "ioswebviewscheme": "moodleappfs", + "appstores": { + "android": "com.moodle.moodlemobile", + "ios": "id633359593", + "windows": "moodle-desktop/9p9bwvhdc8c8", + "mac": "id1255924440", + "linux": "https://download.moodle.org/desktop/download.php?platform=linux&arch=64" + } +} diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index 51cbc8afd..85d063b14 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -41,6 +41,7 @@ import { CORE_PUSHNOTIFICATIONS_PROVIDERS } from '@core/pushnotifications/pushno import { IONIC_NATIVE_PROVIDERS } from '@core/emulator/emulator.module'; import { CORE_EDITOR_PROVIDERS } from '@core/editor/editor.module'; import { CORE_SEARCH_PROVIDERS } from '@core/search/search.module'; +import { CORE_XAPI_PROVIDERS } from '@core/xapi/xapi.module'; // Import only this provider to prevent circular dependencies. import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; @@ -56,7 +57,7 @@ import { Md5 } from 'ts-md5/dist/md5'; // Import core classes that can be useful for site plugins. import { CoreSyncBaseProvider } from '@classes/base-sync'; -import { CoreUrl } from '@classes/utils/url'; +import { CoreUrl } from '@singletons/url'; import { CoreCache } from '@classes/cache'; import { CoreDelegate } from '@classes/delegate'; import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler'; @@ -109,6 +110,7 @@ import { ADDON_MOD_FEEDBACK_PROVIDERS } from '@addon/mod/feedback/feedback.modul import { ADDON_MOD_FOLDER_PROVIDERS } from '@addon/mod/folder/folder.module'; import { ADDON_MOD_FORUM_PROVIDERS } from '@addon/mod/forum/forum.module'; import { ADDON_MOD_GLOSSARY_PROVIDERS } from '@addon/mod/glossary/glossary.module'; +import { ADDON_MOD_H5P_ACTIVITY_PROVIDERS } from '@addon/mod/h5pactivity/h5pactivity.module'; import { ADDON_MOD_IMSCP_PROVIDERS } from '@addon/mod/imscp/imscp.module'; import { ADDON_MOD_LESSON_PROVIDERS } from '@addon/mod/lesson/lesson.module'; import { ADDON_MOD_LTI_PROVIDERS } from '@addon/mod/lti/lti.module'; @@ -242,7 +244,7 @@ export class CoreCompileProvider { .concat(ADDON_MOD_WORKSHOP_PROVIDERS).concat(ADDON_NOTES_PROVIDERS).concat(ADDON_NOTIFICATIONS_PROVIDERS) .concat(CORE_PUSHNOTIFICATIONS_PROVIDERS).concat(ADDON_REMOTETHEMES_PROVIDERS).concat(CORE_BLOCK_PROVIDERS) .concat(CORE_FILTER_PROVIDERS).concat(CORE_H5P_PROVIDERS).concat(CORE_EDITOR_PROVIDERS) - .concat(CORE_SEARCH_PROVIDERS); + .concat(CORE_SEARCH_PROVIDERS).concat(ADDON_MOD_H5P_ACTIVITY_PROVIDERS).concat(CORE_XAPI_PROVIDERS); // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance. for (const i in providers) { diff --git a/src/core/contentlinks/providers/helper.ts b/src/core/contentlinks/providers/helper.ts index 54bb261c1..fc3d45b5f 100644 --- a/src/core/contentlinks/providers/helper.ts +++ b/src/core/contentlinks/providers/helper.ts @@ -32,6 +32,8 @@ import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins import { CoreSite } from '@classes/site'; import { CoreMainMenuProvider } from '@core/mainmenu/providers/mainmenu'; +import { makeSingleton } from '@singletons/core.singletons'; + /** * Service that provides some features regarding content links. */ @@ -358,3 +360,5 @@ export class CoreContentLinksHelperProvider { } } } + +export class CoreContentLinksHelper extends makeSingleton(CoreContentLinksHelperProvider) {} diff --git a/src/core/course/classes/main-activity-component.ts b/src/core/course/classes/main-activity-component.ts index 5b2d7f830..f30bad08d 100644 --- a/src/core/course/classes/main-activity-component.ts +++ b/src/core/course/classes/main-activity-component.ts @@ -14,12 +14,7 @@ import { Injector, Input, NgZone } from '@angular/core'; import { Content } from 'ionic-angular'; -import { CoreSitesProvider } from '@providers/sites'; -import { CoreCourseProvider } from '@core/course/providers/course'; -import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; -import { CoreEventsProvider } from '@providers/events'; import { Network } from '@ionic-native/network'; -import { CoreAppProvider } from '@providers/app'; import { CoreCourseModuleMainResourceComponent } from './main-resource-component'; /** @@ -35,30 +30,13 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR hasOffline: boolean; // If it has offline data to be synced. isOnline: boolean; // If the app is online or not. - protected siteId: string; // Current Site ID. protected syncObserver: any; // It will observe the sync auto event. - protected statusObserver: any; // It will observe changes on the status of the activity. Only if setStatusListener is called. protected onlineObserver: any; // It will observe the status of the network connection. protected syncEventName: string; // Auto sync event name. - protected currentStatus: string; // The current status of the activity. Only if setStatusListener is called. - - // List of services that will be injected using injector. - // It's done like this so subclasses don't have to send all the services to the parent in the constructor. - protected sitesProvider: CoreSitesProvider; - protected courseProvider: CoreCourseProvider; - protected appProvider: CoreAppProvider; - protected eventsProvider: CoreEventsProvider; - protected modulePrefetchDelegate: CoreCourseModulePrefetchDelegate; constructor(injector: Injector, protected content?: Content, loggerName: string = 'CoreCourseModuleMainResourceComponent') { super(injector, loggerName); - this.sitesProvider = injector.get(CoreSitesProvider); - this.courseProvider = injector.get(CoreCourseProvider); - this.appProvider = injector.get(CoreAppProvider); - this.eventsProvider = injector.get(CoreEventsProvider); - this.modulePrefetchDelegate = injector.get(CoreCourseModulePrefetchDelegate); - const network = injector.get(Network); const zone = injector.get(NgZone); @@ -79,7 +57,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR this.hasOffline = false; this.syncIcon = 'spinner'; - this.siteId = this.sitesProvider.getCurrentSiteId(); this.moduleName = this.courseProvider.translateModuleName(this.moduleName); if (this.syncEventName) { @@ -242,44 +219,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR }); } - /** - * Displays some data based on the current status. - * - * @param status The current status. - * @param previousStatus The previous status. If not defined, there is no previous status. - */ - protected showStatus(status: string, previousStatus?: string): void { - // To be overridden. - } - - /** - * Watch for changes on the status. - * - * @return Promise resolved when done. - */ - protected setStatusListener(): Promise { - if (typeof this.statusObserver == 'undefined') { - // Listen for changes on this module status. - this.statusObserver = this.eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => { - if (data.componentId === this.module.id && data.component === this.component) { - // The status has changed, update it. - const previousStatus = this.currentStatus; - this.currentStatus = data.status; - - this.showStatus(this.currentStatus, previousStatus); - } - }, this.siteId); - - // Also, get the current status. - return this.modulePrefetchDelegate.getModuleStatus(this.module, this.courseId).then((status) => { - this.currentStatus = status; - this.showStatus(status); - }); - } - - return Promise.resolve(); - } - /** * Performs the sync of the activity. * @@ -329,6 +268,5 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR this.onlineObserver && this.onlineObserver.unsubscribe(); this.syncObserver && this.syncObserver.off(); - this.statusObserver && this.statusObserver.off(); } } diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts index 50f432894..67a5985d3 100644 --- a/src/core/course/classes/main-resource-component.ts +++ b/src/core/course/classes/main-resource-component.ts @@ -15,16 +15,29 @@ import { OnInit, OnDestroy, Input, Output, EventEmitter, Injector } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider, CoreTextErrorObject } from '@providers/utils/text'; import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseProvider } from '@core/course/providers/course'; import { CoreCourseModuleMainComponent, CoreCourseModuleDelegate } from '@core/course/providers/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseSectionPage } from '@core/course/pages/section/section.ts'; import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper'; import { AddonBlogProvider } from '@addon/blog/providers/blog'; import { CoreConstants } from '@core/constants'; +/** + * Result of a resource download. + */ +export type CoreCourseResourceDownloadResult = { + failed?: boolean; // Whether the download has failed. + error?: string | CoreTextErrorObject; // The error in case it failed. +}; + /** * Template class to easily create CoreCourseModuleMainComponent of resources (or activities without syncing). */ @@ -49,8 +62,12 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected isDestroyed; // Whether the component is destroyed, used when calling fillContextMenu. protected contextMenuStatusObserver; // Observer of package status changed, used when calling fillContextMenu. + protected contextFileStatusObserver; // Observer of file status changed, used when calling fillContextMenu. protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents. protected isCurrentView: boolean; // Whether the component is in the current view. + protected siteId: string; // Current Site ID. + protected statusObserver: any; // It will observe changes on the status of the module. Only if setStatusListener is called. + protected currentStatus: string; // The current status of the module. Only if setStatusListener is called. // List of services that will be injected using injector. // It's done like this so subclasses don't have to send all the services to the parent in the constructor. @@ -63,6 +80,11 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected linkHelper: CoreContentLinksHelperProvider; protected navCtrl: NavController; protected blogProvider: AddonBlogProvider; + protected sitesProvider: CoreSitesProvider; + protected eventsProvider: CoreEventsProvider; + protected modulePrefetchDelegate: CoreCourseModulePrefetchDelegate; + protected courseProvider: CoreCourseProvider; + protected appProvider: CoreAppProvider; protected logger; @@ -76,6 +98,12 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, this.linkHelper = injector.get(CoreContentLinksHelperProvider); this.navCtrl = injector.get(NavController, null); this.blogProvider = injector.get(AddonBlogProvider, null); + this.sitesProvider = injector.get(CoreSitesProvider); + this.eventsProvider = injector.get(CoreEventsProvider); + this.modulePrefetchDelegate = injector.get(CoreCourseModulePrefetchDelegate); + this.courseProvider = injector.get(CoreCourseProvider); + this.appProvider = injector.get(CoreAppProvider); + this.dataRetrieved = new EventEmitter(); const loggerProvider = injector.get(CoreLoggerProvider); @@ -86,6 +114,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * Component being initialized. */ ngOnInit(): void { + this.siteId = this.sitesProvider.getCurrentSiteId(); this.description = this.module.description; this.componentId = this.module.id; this.externalUrl = this.module.url; @@ -236,8 +265,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * Expand the description. */ expandDescription(): void { - this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component, this.module.id, - [], true, 'module', this.module.id, this.courseId); + this.textUtils.viewText(this.translate.instant('core.description'), this.description, { + component: this.component, + componentId: this.module.id, + filter: true, + contextLevel: 'module', + instanceId: this.module.id, + courseId: this.courseId, + }); } /** @@ -245,8 +280,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * * @param event Event. */ - gotoBlog(event: any): void { - this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id }); + gotoBlog(event: any): Promise { + return this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id }); } /** @@ -260,9 +295,36 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, /** * Confirm and remove downloaded files. + * + * @param done Function to call when done. */ - removeFiles(): void { - this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId); + removeFiles(done?: () => void): void { + if (this.prefetchStatus == CoreConstants.DOWNLOADING) { + this.domUtils.showAlertTranslated(null, 'core.course.cannotdeletewhiledownloading'); + + return; + } + + this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId, done); + } + + /** + * Get message about an error occurred while downloading files. + * + * @param error The specific error. + * @param multiLine Whether to put each message in a different paragraph or in a single line. + */ + protected getErrorDownloadingSomeFilesMessage(error: string | CoreTextErrorObject, multiLine?: boolean): string { + if (multiLine) { + return this.textUtils.buildSeveralParagraphsMessage([ + this.translate.instant('core.errordownloadingsomefiles'), + error, + ]); + } else { + error = this.textUtils.getErrorMessageFromError(error); + + return this.translate.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : ''); + } } /** @@ -271,12 +333,97 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, * @param error The specific error. */ protected showErrorDownloadingSomeFiles(error: string | CoreTextErrorObject): void { - const errorMessage = this.textUtils.buildSeveralParagraphsMessage([ - this.translate.instant('core.errordownloadingsomefiles'), - error, - ]); + this.domUtils.showErrorModal(this.getErrorDownloadingSomeFilesMessage(error, true)); + } - this.domUtils.showErrorModal(errorMessage); + /** + * Displays some data based on the current status. + * + * @param status The current status. + * @param previousStatus The previous status. If not defined, there is no previous status. + */ + protected showStatus(status: string, previousStatus?: string): void { + // To be overridden. + } + + /** + * Watch for changes on the status. + * + * @return Promise resolved when done. + */ + protected setStatusListener(): Promise { + if (typeof this.statusObserver == 'undefined') { + // Listen for changes on this module status. + this.statusObserver = this.eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => { + if (data.componentId === this.module.id && data.component === this.component) { + // The status has changed, update it. + const previousStatus = this.currentStatus; + this.currentStatus = data.status; + + this.showStatus(this.currentStatus, previousStatus); + } + }, this.siteId); + + // Also, get the current status. + return this.modulePrefetchDelegate.getModuleStatus(this.module, this.courseId).then((status) => { + this.currentStatus = status; + this.showStatus(status); + }); + } + + return Promise.resolve(); + } + + /** + * Download a resource if needed. + * If the download call fails the promise won't be rejected, but the error will be included in the returned object. + * If module.contents cannot be loaded then the Promise will be rejected. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async downloadResourceIfNeeded(refresh?: boolean, contentsAlreadyLoaded?: boolean) + : Promise { + + const result: CoreCourseResourceDownloadResult = { + failed: false, + }; + + // Get module status to determine if it needs to be downloaded. + await this.setStatusListener(); + + if (this.currentStatus != CoreConstants.DOWNLOADED) { + // Download content. This function also loads module contents if needed. + try { + await this.modulePrefetchDelegate.downloadModule(this.module, this.courseId); + + // If we reach here it means the download process already loaded the contents, no need to do it again. + contentsAlreadyLoaded = true; + } catch (error) { + // Mark download as failed but go on since the main files could have been downloaded. + result.failed = true; + result.error = error; + } + } + + if (!this.module.contents.length || (refresh && !contentsAlreadyLoaded)) { + // Try to load the contents. + const ignoreCache = refresh && this.appProvider.isOnline(); + + try { + await this.courseProvider.loadModuleContents(this.module, this.courseId, undefined, false, ignoreCache); + } catch (error) { + // Error loading contents. If we ignored cache, try to get the cached value. + if (ignoreCache && !this.module.contents) { + await this.courseProvider.loadModuleContents(this.module, this.courseId); + } else if (!this.module.contents) { + // Not able to load contents, throw the error. + throw error; + } + } + } + + return result; } /** @@ -285,6 +432,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, ngOnDestroy(): void { this.isDestroyed = true; this.contextMenuStatusObserver && this.contextMenuStatusObserver.off(); + this.contextFileStatusObserver && this.contextFileStatusObserver.off(); + this.statusObserver && this.statusObserver.off(); } /** diff --git a/src/core/course/classes/module-prefetch-handler.ts b/src/core/course/classes/module-prefetch-handler.ts index 815cbf5c4..e51ba6e0a 100644 --- a/src/core/course/classes/module-prefetch-handler.ts +++ b/src/core/course/classes/module-prefetch-handler.ts @@ -19,6 +19,7 @@ import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseProvider } from '../providers/course'; +import { CoreWSExternalFile } from '@providers/ws'; import { CoreCourseModulePrefetchHandler } from '../providers/module-prefetch-delegate'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; @@ -114,7 +115,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @param module The module object returned by WS. * @return List of files. */ - getContentDownloadableFiles(module: any): any[] { + getContentDownloadableFiles(module: any): CoreWSExternalFile[] { const files = []; if (module.contents && module.contents.length) { @@ -139,7 +140,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref */ getDownloadSize(module: any, courseId: number, single?: boolean): Promise<{ size: number, total: boolean }> { return this.getFiles(module, courseId).then((files) => { - return this.pluginFileDelegate.getFilesSize(files); + return this.pluginFileDelegate.getFilesDownloadSize(files); }).catch(() => { return { size: -1, total: false }; }); @@ -166,7 +167,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @param single True if we're downloading a single module, false if we're downloading a whole section. * @return Promise resolved with the list of files. */ - getFiles(module: any, courseId: number, single?: boolean): Promise { + getFiles(module: any, courseId: number, single?: boolean): Promise { // To be overridden. return Promise.resolve([]); } @@ -179,7 +180,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved with list of intro files. */ - getIntroFiles(module: any, courseId: number, ignoreCache?: boolean): Promise { + getIntroFiles(module: any, courseId: number, ignoreCache?: boolean): Promise { return Promise.resolve(this.getIntroFilesFromInstance(module)); } @@ -190,7 +191,7 @@ export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePref * @param instance The instance to get the intro files (book, assign, ...). If not defined, module will be used. * @return List of intro files. */ - getIntroFilesFromInstance(module: any, instance?: any): any[] { + getIntroFilesFromInstance(module: any, instance?: any): CoreWSExternalFile[] { if (instance) { if (typeof instance.introfiles != 'undefined') { return instance.introfiles; diff --git a/src/core/course/components/module/core-course-module.html b/src/core/course/components/module/core-course-module.html index 39358083f..9494aed55 100644 --- a/src/core/course/components/module/core-course-module.html +++ b/src/core/course/components/module/core-course-module.html @@ -1,8 +1,8 @@ - +
    - +
    @@ -13,7 +13,7 @@ -
    diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json index 2fe5518e9..69cbe5cf9 100644 --- a/src/core/course/lang/en.json +++ b/src/core/course/lang/en.json @@ -5,6 +5,7 @@ "allsections": "All sections", "askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.", "availablespace": " You currently have about {{available}} free space.", + "cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", "confirmdeletemodulefiles": "Are you sure you want to delete these files?", "confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", "confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?", diff --git a/src/core/course/pages/unsupported-module/unsupported-module.ts b/src/core/course/pages/unsupported-module/unsupported-module.ts index a28a2dde6..e63edc461 100644 --- a/src/core/course/pages/unsupported-module/unsupported-module.ts +++ b/src/core/course/pages/unsupported-module/unsupported-module.ts @@ -38,7 +38,11 @@ export class CoreCourseUnsupportedModulePage { * Expand the description. */ expandDescription(): void { - this.textUtils.expandText(this.translate.instant('core.description'), this.module.description, undefined, undefined, - [], true, 'module', this.module.id, this.courseId); + this.textUtils.viewText(this.translate.instant('core.description'), this.module.description, { + filter: true, + contextLevel: 'module', + instanceId: this.module.id, + courseId: this.courseId, + }); } } diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts index c6f99929b..98c551149 100644 --- a/src/core/course/providers/course.ts +++ b/src/core/course/providers/course.ts @@ -28,6 +28,8 @@ import { CoreCourseOfflineProvider } from './course-offline'; import { CoreSitePluginsProvider } from '@core/siteplugins/providers/siteplugins'; import { CoreCourseFormatDelegate } from './format-delegate'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; +import { CoreCoursesProvider } from '@core/courses/providers/courses'; +import { makeSingleton } from '@singletons/core.singletons'; /** * Service that provides some features regarding a course. @@ -97,7 +99,7 @@ export class CoreCourseProvider { protected CORE_MODULES = [ 'assign', 'assignment', 'book', 'chat', 'choice', 'data', 'database', 'date', 'external-tool', 'feedback', 'file', 'folder', 'forum', 'glossary', 'ims', 'imscp', 'label', 'lesson', 'lti', 'page', 'quiz', - 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop' + 'resource', 'scorm', 'survey', 'url', 'wiki', 'workshop', 'h5pactivity' ]; constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider, @@ -333,6 +335,23 @@ export class CoreCourseProvider { }); } + /** + * Obtain ids of downloaded courses. + * + * @param siteId Site id. + * @return Resolves with an array containing downloaded course ids. + */ + async getDownloadedCourseIds(siteId?: string): Promise { + const site = await this.sitesProvider.getSite(siteId); + const entries = await site.getDb().getRecordsList(this.COURSE_STATUS_TABLE, 'status', [ + CoreConstants.DOWNLOADED, + CoreConstants.DOWNLOADING, + CoreConstants.OUTDATED, + ]); + + return entries.map((entry) => entry.id); + } + /** * Get a module from Moodle. * @@ -860,6 +879,11 @@ export class CoreCourseProvider { return site.write('core_course_view_course', params).then((response) => { if (!response.status) { return Promise.reject(null); + } else { + this.eventsProvider.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: courseId, + action: CoreCoursesProvider.ACTION_VIEW, + }, site.getId()); } }); }); @@ -1172,3 +1196,5 @@ export type CoreCourseModuleSummary = { url?: string; // Url. iconurl: string; // Iconurl. }; + +export class CoreCourse extends makeSingleton(CoreCourseProvider) {} diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 7fbc8b740..25d5137eb 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -13,17 +13,18 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { NavController } from 'ionic-angular'; +import { NavController, Loading } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; -import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreFilepoolProvider, CoreFilepoolComponentFileEventData } from '@providers/filepool'; import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; +import { CoreUrlUtils } from '@providers/utils/url'; import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay, CoreCourseOptionsMenuHandlerToDisplay } from './options-delegate'; @@ -39,6 +40,8 @@ import { CoreSite } from '@classes/site'; import { CoreLoggerProvider } from '@providers/logger'; import * as moment from 'moment'; import { CoreFilterHelperProvider } from '@core/filter/providers/helper'; +import { CoreArray } from '@singletons/array'; +import { makeSingleton } from '@singletons/core.singletons'; /** * Prefetch info of a module. @@ -408,16 +411,29 @@ export class CoreCourseHelperProvider { * * @param module Module to remove the files. * @param courseId Course ID the module belongs to. + * @param done Function to call when done. It will close the context menu. * @return Promise resolved when done. */ - confirmAndRemoveFiles(module: any, courseId: number): Promise { - return this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles').then(() => { - return this.prefetchDelegate.removeModuleFiles(module, courseId); - }).catch((error) => { + async confirmAndRemoveFiles(module: any, courseId: number, done?: () => void): Promise { + let modal: Loading; + + try { + + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + + modal = this.domUtils.showModalLoading(); + + await this.prefetchDelegate.removeModuleFiles(module, courseId); + + done && done(); + + } catch (error) { if (error) { this.domUtils.showErrorModal(error); } - }); + } finally { + modal && modal.dismiss(); + } } /** @@ -602,7 +618,8 @@ export class CoreCourseHelperProvider { // File shouldn't be opened in browser. Download the module if it needs to be downloaded. return this.downloadModuleWithMainFileIfNeeded(module, courseId, component, componentId, files, siteId) .then((result) => { - if (result.path.indexOf('http') === 0) { + + if (!CoreUrlUtils.instance.isLocalFileUrl(result.path)) { /* In iOS, if we use the same URL in embedded browser and background download then the download only downloads a few bytes (cached ones). Add a hash to the URL so both URLs are different. */ result.path = result.path + '#moodlemobile-embedded'; @@ -800,6 +817,8 @@ export class CoreCourseHelperProvider { * @return Promise resolved when done. */ fillContextMenu(instance: any, module: any, courseId: number, invalidateCache?: boolean, component?: string): Promise { + const siteId = this.sitesProvider.getCurrentSiteId(); + return this.getModulePrefetchInfo(module, courseId, invalidateCache, component).then((moduleInfo) => { instance.size = moduleInfo.size > 0 ? moduleInfo.sizeReadable : 0; instance.prefetchStatusIcon = moduleInfo.statusIcon; @@ -825,7 +844,32 @@ export class CoreCourseHelperProvider { if (data.componentId == module.id && data.component == component) { this.fillContextMenu(instance, module, courseId, false, component); } - }, this.sitesProvider.getCurrentSiteId()); + }, siteId); + } + + if (typeof instance.contextFileStatusObserver == 'undefined' && component) { + // Debounce the update size function to prevent too many calls when downloading or deleting a whole activity. + const debouncedUpdateSize = this.utils.debounce(() => { + this.prefetchDelegate.getModuleDownloadedSize(module, courseId).then((moduleSize) => { + instance.size = moduleSize > 0 ? this.textUtils.bytesToSize(moduleSize, 2) : 0; + }); + }, 1000); + + instance.contextFileStatusObserver = this.eventsProvider.on(CoreEventsProvider.COMPONENT_FILE_ACTION, + (data: CoreFilepoolComponentFileEventData) => { + + if (data.component != component || data.componentId != module.id) { + // The event doesn't belong to this component, ignore. + return; + } + + if (!this.filepoolProvider.isFileEventDownloadedOrDeleted(data)) { + return; + } + + // Update the module size. + debouncedUpdateSize(); + }, siteId); } }); } @@ -1229,14 +1273,17 @@ export class CoreCourseHelperProvider { // Get the module. return this.courseProvider.getModule(moduleId, courseId, sectionId, false, false, siteId, modName); }).then((module) => { - module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId, false); - if (navCtrl && module.handlerData && module.handlerData.action) { + if (navCtrl && this.sitesProvider.isLoggedIn() && this.sitesProvider.getCurrentSiteId() == site.getId()) { // If the link handler for this module passed through navCtrl, we can use the module's handler to navigate cleanly. // Otherwise, we will redirect below. - modal.dismiss(); + module.handlerData = this.moduleDelegate.getModuleDataFor(module.modname, module, courseId, sectionId, false); - return module.handlerData.action(new Event('click'), navCtrl, module, courseId, undefined, modParams); + if (module.handlerData && module.handlerData.action) { + modal.dismiss(); + + return module.handlerData.action(new Event('click'), navCtrl, module, courseId, undefined, modParams); + } } this.logger.warn('navCtrl was not passed to navigateToModule by the link handler for ' + module.modname); @@ -1562,4 +1609,24 @@ export class CoreCourseHelperProvider { return this.loginHelper.redirect(CoreLoginHelperProvider.OPEN_COURSE, params, siteId); } } + + /** + * Delete course files. + * + * @param courseId Course id. + * @return Promise to be resolved once the course files are deleted. + */ + async deleteCourseFiles(courseId: number): Promise { + const sections = await this.courseProvider.getSections(courseId); + const modules = CoreArray.flatten(sections.map((section) => section.modules)); + + await Promise.all( + modules.map((module) => this.prefetchDelegate.removeModuleFiles(module, courseId)), + ); + + await this.courseProvider.setCourseStatus(courseId, CoreConstants.NOT_DOWNLOADED); + } + } + +export class CoreCourseHelper extends makeSingleton(CoreCourseHelperProvider) {} diff --git a/src/core/course/providers/log-helper.ts b/src/core/course/providers/log-helper.ts index 8792eab81..d10aeaf43 100644 --- a/src/core/course/providers/log-helper.ts +++ b/src/core/course/providers/log-helper.ts @@ -20,6 +20,8 @@ import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreAppProvider } from '@providers/app'; import { CorePushNotificationsProvider } from '@core/pushnotifications/providers/pushnotifications'; +import { makeSingleton } from '@singletons/core.singletons'; + /** * Helper to manage logging to Moodle. */ @@ -355,3 +357,5 @@ export class CoreCourseLogHelperProvider { })); } } + +export class CoreCourseLogHelper extends makeSingleton(CoreCourseLogHelperProvider) {} diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 6e05b283b..917a7bc4f 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; -import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreFilepoolProvider, CoreFilepoolComponentFileEventData } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -28,6 +28,7 @@ import { Md5 } from 'ts-md5/dist/md5'; import { Subject, BehaviorSubject, Subscription } from 'rxjs'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreFileHelperProvider } from '@providers/file-helper'; +import { makeSingleton } from '@singletons/core.singletons'; /** * Progress of downloading a list of modules. @@ -243,6 +244,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { protected ROOT_CACHE_KEY = 'mmCourse:'; protected statusCache = new CoreCache(); + protected featurePrefix = 'CoreCourseModuleDelegate_'; protected handlerNameProperty = 'modName'; // Promises for check updates, to prevent performing the same request twice at the same time. @@ -276,6 +278,15 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => { this.updateStatusCache(data.status, data.component, data.componentId); }, this.sitesProvider.getCurrentSiteId()); + + // If a file inside a module is downloaded/deleted, clear the corresponding cache. + eventsProvider.on(CoreEventsProvider.COMPONENT_FILE_ACTION, (data: CoreFilepoolComponentFileEventData) => { + if (!this.filepoolProvider.isFileEventDownloadedOrDeleted(data)) { + return; + } + + this.statusCache.invalidate(this.filepoolProvider.getPackageId(data.component, data.componentId)); + }, this.sitesProvider.getCurrentSiteId()); } /** @@ -395,6 +406,25 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { return status; } + /** + * Download a module. + * + * @param module Module to download. + * @param courseId Course ID the module belongs to. + * @param dirPath Path of the directory where to store all the content files. + * @return Promise resolved when finished. + */ + async downloadModule(module: any, courseId: number, dirPath?: string): Promise { + const handler = this.getPrefetchHandlerFor(module); + + // Check if the module has a prefetch handler. + if (handler) { + await this.syncModule(module, courseId); + + await handler.download(module, courseId, dirPath); + } + } + /** * Check for updates in a course. * @@ -1439,3 +1469,5 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { } } } + +export class CoreCourseModulePrefetch extends makeSingleton(CoreCourseModulePrefetchDelegate) {} diff --git a/src/core/course/providers/options-delegate.ts b/src/core/course/providers/options-delegate.ts index 9d2745056..815be116e 100644 --- a/src/core/course/providers/options-delegate.ts +++ b/src/core/course/providers/options-delegate.ts @@ -243,11 +243,17 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { */ protected clearCoursesHandlers(courseId?: number): void { if (courseId) { + if (!this.loaded[courseId]) { + // Don't clear if not loaded, it's probably an ongoing load and it could cause JS errors. + return; + } + this.loaded[courseId] = false; delete this.coursesHandlers[courseId]; } else { - this.loaded = {}; - this.coursesHandlers = {}; + for (const courseId in this.coursesHandlers) { + this.clearCoursesHandlers(Number(courseId)); + } } } @@ -484,7 +490,7 @@ export class CoreCourseOptionsDelegate extends CoreDelegate { const promises = [], courseData = this.coursesHandlers[courseId]; - if (!courseData) { + if (!courseData || !courseData.enabledHandlers) { return Promise.resolve(); } diff --git a/src/core/courses/components/course-options-menu/core-courses-course-options-menu.html b/src/core/courses/components/course-options-menu/core-courses-course-options-menu.html index 9e17624b8..53682fd43 100644 --- a/src/core/courses/components/course-options-menu/core-courses-course-options-menu.html +++ b/src/core/courses/components/course-options-menu/core-courses-course-options-menu.html @@ -3,6 +3,10 @@

    {{ prefetch.title | translate }}

    + + +

    {{ 'addon.storagemanager.deletecourse' | translate }}

    +

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

    diff --git a/src/core/courses/components/course-options-menu/course-options-menu.ts b/src/core/courses/components/course-options-menu/course-options-menu.ts index 02aae3630..fba1d2de2 100644 --- a/src/core/courses/components/course-options-menu/course-options-menu.ts +++ b/src/core/courses/components/course-options-menu/course-options-menu.ts @@ -15,6 +15,7 @@ import { Component, OnInit } from '@angular/core'; import { NavParams, ViewController } from 'ionic-angular'; import { CoreCoursesProvider } from '../../providers/courses'; +import { CoreConstants } from '@core/constants'; /** * This component is meant to display a popover with the course options. @@ -25,12 +26,14 @@ import { CoreCoursesProvider } from '../../providers/courses'; }) export class CoreCoursesCourseOptionsMenuComponent implements OnInit { course: any; // The course. + courseStatus: string; // The course status. prefetch: any; // The prefecth info. downloadCourseEnabled: boolean; constructor(navParams: NavParams, private viewCtrl: ViewController, private coursesProvider: CoreCoursesProvider) { this.course = navParams.get('course') || {}; + this.courseStatus = navParams.get('courseStatus') || CoreConstants.NOT_DOWNLOADED; this.prefetch = navParams.get('prefetch') || {}; } diff --git a/src/core/courses/components/course-progress/core-courses-course-progress.html b/src/core/courses/components/course-progress/core-courses-course-progress.html index 6818d7be4..846831983 100644 --- a/src/core/courses/components/course-progress/core-courses-course-progress.html +++ b/src/core/courses/components/course-progress/core-courses-course-progress.html @@ -3,7 +3,7 @@
    -
    +

    | @@ -30,7 +30,7 @@ - + + + diff --git a/src/core/courses/pages/dashboard/dashboard.ts b/src/core/courses/pages/dashboard/dashboard.ts index 99ade84ca..49f2924bb 100644 --- a/src/core/courses/pages/dashboard/dashboard.ts +++ b/src/core/courses/pages/dashboard/dashboard.ts @@ -122,6 +122,13 @@ export class CoreCoursesDashboardPage implements OnDestroy { this.tabsComponent && this.tabsComponent.ionViewDidLeave(); } + /** + * Open page to manage courses storage. + */ + manageCoursesStorage(): void { + this.navCtrl.push('AddonStorageManagerCoursesStoragePage'); + } + /** * Go to search courses. */ diff --git a/src/core/courses/providers/courses.ts b/src/core/courses/providers/courses.ts index b0529d926..fc56eb385 100644 --- a/src/core/courses/providers/courses.ts +++ b/src/core/courses/providers/courses.ts @@ -17,6 +17,18 @@ import { CoreEventsProvider } from '@providers/events'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSitesReadingStrategy } from '@providers/sites'; import { CoreSite } from '@classes/site'; +import { makeSingleton } from '@singletons/core.singletons'; + +/** + * Data sent to the EVENT_MY_COURSES_UPDATED. + */ +export type CoreCoursesMyCoursesUpdatedEventData = { + action: string; // Action performed. + courseId?: number; // Course ID affected (if any). + course?: any; // Course affected (if any). + state?: string; // Only for ACTION_STATE_CHANGED. The state that changed (hidden, favourite). + value?: boolean; // The new value for the state changed. +}; /** * Service that provides some features regarding lists of courses and categories. @@ -30,6 +42,15 @@ export class CoreCoursesProvider { static EVENT_MY_COURSES_REFRESHED = 'courses_my_courses_refreshed'; static EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED = 'dashboard_download_enabled_changed'; + // Actions for event EVENT_MY_COURSES_UPDATED. + static ACTION_ENROL = 'enrol'; // User enrolled in a course. + static ACTION_STATE_CHANGED = 'state_changed'; // Course state changed (hidden, favourite). + static ACTION_VIEW = 'view'; // Course viewed. + + // Possible states changed. + static STATE_HIDDEN = 'hidden'; + static STATE_FAVOURITE = 'favourite'; + protected ROOT_CACHE_KEY = 'mmCourses:'; protected logger; protected userCoursesIds: {[id: number]: boolean}; // Use an object to make it faster to search. @@ -1132,3 +1153,5 @@ export class CoreCoursesProvider { }); } } + +export class CoreCourses extends makeSingleton(CoreCoursesProvider) {} diff --git a/src/core/courses/providers/helper.ts b/src/core/courses/providers/helper.ts index 30e3ab092..d7ac68a4b 100644 --- a/src/core/courses/providers/helper.ts +++ b/src/core/courses/providers/helper.ts @@ -212,6 +212,14 @@ export class CoreCoursesHelperProvider { return b.timemodified - a.timemodified; }); break; + case 'shortname': + courses.sort((a, b) => { + const compareA = a.shortname.toLowerCase(), + compareB = b.shortname.toLowerCase(); + + return compareA.localeCompare(compareB); + }); + break; default: // Sort not implemented. Do not sort. } diff --git a/src/core/editor/components/rich-text-editor/core-editor-rich-text-editor.html b/src/core/editor/components/rich-text-editor/core-editor-rich-text-editor.html index 20faba16e..a193a8dbe 100644 --- a/src/core/editor/components/rich-text-editor/core-editor-rich-text-editor.html +++ b/src/core/editor/components/rich-text-editor/core-editor-rich-text-editor.html @@ -17,12 +17,12 @@ - - @@ -71,6 +71,11 @@ + + +