commit
20c2770c12
|
@ -170,6 +170,7 @@
|
||||||
<plugin name="cordova-plugin-local-notification" spec="https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle" />
|
<plugin name="cordova-plugin-local-notification" spec="https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle" />
|
||||||
<plugin name="cordova-plugin-media-capture" spec="3.0.3" />
|
<plugin name="cordova-plugin-media-capture" spec="3.0.3" />
|
||||||
<plugin name="cordova-plugin-network-information" spec="2.0.2" />
|
<plugin name="cordova-plugin-network-information" spec="2.0.2" />
|
||||||
|
<plugin name="cordova-plugin-qrscanner" spec="https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist" />
|
||||||
<plugin name="cordova-plugin-screen-orientation" spec="3.0.2" />
|
<plugin name="cordova-plugin-screen-orientation" spec="3.0.2" />
|
||||||
<plugin name="cordova-plugin-splashscreen" spec="5.0.3" />
|
<plugin name="cordova-plugin-splashscreen" spec="5.0.3" />
|
||||||
<plugin name="cordova-plugin-statusbar" spec="2.4.3" />
|
<plugin name="cordova-plugin-statusbar" spec="2.4.3" />
|
||||||
|
|
|
@ -182,6 +182,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@ionic-native/push/-/push-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic-native/push/-/push-4.20.0.tgz",
|
||||||
"integrity": "sha512-IgzaZd8KSPLwyLX1emRijlQ0Vfa3RlPPBx370lVH32c8zG3DFH1xfQQbb39KF3qmX5b6so0pGGA2holSUwVm2w=="
|
"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": {
|
"@ionic-native/screen-orientation": {
|
||||||
"version": "4.20.0",
|
"version": "4.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ionic-native/screen-orientation/-/screen-orientation-4.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ionic-native/screen-orientation/-/screen-orientation-4.20.0.tgz",
|
||||||
|
@ -2705,6 +2710,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/cordova-plugin-network-information/-/cordova-plugin-network-information-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/cordova-plugin-network-information/-/cordova-plugin-network-information-2.0.2.tgz",
|
||||||
"integrity": "sha512-NwO3qDBNL/vJxUxBTPNOA1HvkDf9eTeGH8JSZiwy1jq2W2mJKQEDBwqWkaEQS19Yd/MQTiw0cykxg5D7u4J6cQ=="
|
"integrity": "sha512-NwO3qDBNL/vJxUxBTPNOA1HvkDf9eTeGH8JSZiwy1jq2W2mJKQEDBwqWkaEQS19Yd/MQTiw0cykxg5D7u4J6cQ=="
|
||||||
},
|
},
|
||||||
|
"cordova-plugin-qrscanner": {
|
||||||
|
"version": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#43952839ce97887d1c6cad53c7d668fe3370aedd",
|
||||||
|
"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": {
|
"cordova-plugin-screen-orientation": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/cordova-plugin-screen-orientation/-/cordova-plugin-screen-orientation-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/cordova-plugin-screen-orientation/-/cordova-plugin-screen-orientation-3.0.2.tgz",
|
||||||
|
@ -10224,6 +10237,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
|
||||||
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
|
"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": {
|
"qs": {
|
||||||
"version": "6.5.2",
|
"version": "6.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||||
|
@ -11127,6 +11145,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sdp": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/sdp/-/sdp-1.5.4.tgz",
|
||||||
|
"integrity": "sha1-jgOPbdsUvXZa4fS1IW4SCUUR4NA="
|
||||||
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||||
|
@ -13864,6 +13887,14 @@
|
||||||
"source-map": "~0.6.1"
|
"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": {
|
"websocket-driver": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
"@ionic-native/media-capture": "4.20.0",
|
"@ionic-native/media-capture": "4.20.0",
|
||||||
"@ionic-native/network": "4.20.0",
|
"@ionic-native/network": "4.20.0",
|
||||||
"@ionic-native/push": "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/screen-orientation": "4.20.0",
|
||||||
"@ionic-native/splash-screen": "4.20.0",
|
"@ionic-native/splash-screen": "4.20.0",
|
||||||
"@ionic-native/sqlite": "4.20.0",
|
"@ionic-native/sqlite": "4.20.0",
|
||||||
|
@ -98,6 +99,7 @@
|
||||||
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
|
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
|
||||||
"cordova-plugin-media-capture": "3.0.3",
|
"cordova-plugin-media-capture": "3.0.3",
|
||||||
"cordova-plugin-network-information": "2.0.2",
|
"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-screen-orientation": "3.0.2",
|
||||||
"cordova-plugin-splashscreen": "5.0.3",
|
"cordova-plugin-splashscreen": "5.0.3",
|
||||||
"cordova-plugin-statusbar": "2.4.3",
|
"cordova-plugin-statusbar": "2.4.3",
|
||||||
|
@ -194,7 +196,8 @@
|
||||||
"cordova-plugin-advanced-http": {
|
"cordova-plugin-advanced-http": {
|
||||||
"OKHTTP_VERSION": "3.10.0"
|
"OKHTTP_VERSION": "3.10.0"
|
||||||
},
|
},
|
||||||
"cordova-plugin-wkwebview-cookies": {}
|
"cordova-plugin-wkwebview-cookies": {},
|
||||||
|
"cordova-plugin-qrscanner": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"main": "desktop/electron.js",
|
"main": "desktop/electron.js",
|
||||||
|
@ -254,4 +257,4 @@
|
||||||
"deleteAppDataOnUninstall": true
|
"deleteAppDataOnUninstall": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1333,6 +1333,8 @@
|
||||||
"core.browser": "local_moodlemobileapp",
|
"core.browser": "local_moodlemobileapp",
|
||||||
"core.cancel": "moodle",
|
"core.cancel": "moodle",
|
||||||
"core.cannotconnect": "local_moodlemobileapp",
|
"core.cannotconnect": "local_moodlemobileapp",
|
||||||
|
"core.cannotconnecttrouble": "local_moodlemobileapp",
|
||||||
|
"core.cannotconnectverify": "local_moodlemobileapp",
|
||||||
"core.cannotdownloadfiles": "local_moodlemobileapp",
|
"core.cannotdownloadfiles": "local_moodlemobileapp",
|
||||||
"core.captureaudio": "local_moodlemobileapp",
|
"core.captureaudio": "local_moodlemobileapp",
|
||||||
"core.capturedimage": "local_moodlemobileapp",
|
"core.capturedimage": "local_moodlemobileapp",
|
||||||
|
@ -1385,6 +1387,7 @@
|
||||||
"core.contentlinks.errorredirectothersite": "local_moodlemobileapp",
|
"core.contentlinks.errorredirectothersite": "local_moodlemobileapp",
|
||||||
"core.continue": "moodle",
|
"core.continue": "moodle",
|
||||||
"core.copiedtoclipboard": "local_moodlemobileapp",
|
"core.copiedtoclipboard": "local_moodlemobileapp",
|
||||||
|
"core.copytoclipboard": "local_moodlemobileapp",
|
||||||
"core.course": "moodle",
|
"core.course": "moodle",
|
||||||
"core.course.activitydisabled": "local_moodlemobileapp",
|
"core.course.activitydisabled": "local_moodlemobileapp",
|
||||||
"core.course.activitynotyetviewableremoteaddon": "local_moodlemobileapp",
|
"core.course.activitynotyetviewableremoteaddon": "local_moodlemobileapp",
|
||||||
|
@ -1721,6 +1724,8 @@
|
||||||
"core.login.faqtestappquestion": "local_moodlemobileapp",
|
"core.login.faqtestappquestion": "local_moodlemobileapp",
|
||||||
"core.login.faqwhatisurlanswer": "local_moodlemobileapp",
|
"core.login.faqwhatisurlanswer": "local_moodlemobileapp",
|
||||||
"core.login.faqwhatisurlquestion": "local_moodlemobileapp",
|
"core.login.faqwhatisurlquestion": "local_moodlemobileapp",
|
||||||
|
"core.login.faqwhereisqrcode": "local_moodlemobileapp",
|
||||||
|
"core.login.faqwhereisqrcodeanswer": "local_moodlemobileapp",
|
||||||
"core.login.findyoursite": "local_moodlemobileapp",
|
"core.login.findyoursite": "local_moodlemobileapp",
|
||||||
"core.login.firsttime": "moodle",
|
"core.login.firsttime": "moodle",
|
||||||
"core.login.forcepasswordchangenotice": "moodle",
|
"core.login.forcepasswordchangenotice": "moodle",
|
||||||
|
@ -1750,6 +1755,7 @@
|
||||||
"core.login.mustconfirm": "moodle",
|
"core.login.mustconfirm": "moodle",
|
||||||
"core.login.newaccount": "moodle",
|
"core.login.newaccount": "moodle",
|
||||||
"core.login.notloggedin": "local_moodlemobileapp",
|
"core.login.notloggedin": "local_moodlemobileapp",
|
||||||
|
"core.login.or": "local_moodlemobileapp",
|
||||||
"core.login.password": "moodle",
|
"core.login.password": "moodle",
|
||||||
"core.login.passwordforgotten": "moodle",
|
"core.login.passwordforgotten": "moodle",
|
||||||
"core.login.passwordforgotteninstructions2": "moodle",
|
"core.login.passwordforgotteninstructions2": "moodle",
|
||||||
|
@ -1759,8 +1765,6 @@
|
||||||
"core.login.policyagreement": "moodle",
|
"core.login.policyagreement": "moodle",
|
||||||
"core.login.policyagreementclick": "moodle",
|
"core.login.policyagreementclick": "moodle",
|
||||||
"core.login.potentialidps": "auth",
|
"core.login.potentialidps": "auth",
|
||||||
"core.login.problemconnectingerror": "local_moodlemobileapp",
|
|
||||||
"core.login.problemconnectingerrorcontinue": "local_moodlemobileapp",
|
|
||||||
"core.login.profileinvaliddata": "admin",
|
"core.login.profileinvaliddata": "admin",
|
||||||
"core.login.recaptchachallengeimage": "local_moodlemobileapp",
|
"core.login.recaptchachallengeimage": "local_moodlemobileapp",
|
||||||
"core.login.recaptchaexpired": "local_moodlemobileapp",
|
"core.login.recaptchaexpired": "local_moodlemobileapp",
|
||||||
|
@ -1789,6 +1793,8 @@
|
||||||
"core.login.usernotaddederror": "error",
|
"core.login.usernotaddederror": "error",
|
||||||
"core.login.visitchangepassword": "local_moodlemobileapp",
|
"core.login.visitchangepassword": "local_moodlemobileapp",
|
||||||
"core.login.webservicesnotenabled": "local_moodlemobileapp",
|
"core.login.webservicesnotenabled": "local_moodlemobileapp",
|
||||||
|
"core.login.youcanstillconnectwithcredentials": "local_moodlemobileapp",
|
||||||
|
"core.login.yourenteredsite": "local_moodlemobileapp",
|
||||||
"core.lostconnection": "local_moodlemobileapp",
|
"core.lostconnection": "local_moodlemobileapp",
|
||||||
"core.mainmenu.changesite": "local_moodlemobileapp",
|
"core.mainmenu.changesite": "local_moodlemobileapp",
|
||||||
"core.mainmenu.help": "moodle",
|
"core.mainmenu.help": "moodle",
|
||||||
|
@ -1856,6 +1862,7 @@
|
||||||
"core.online": "message",
|
"core.online": "message",
|
||||||
"core.openfullimage": "local_moodlemobileapp",
|
"core.openfullimage": "local_moodlemobileapp",
|
||||||
"core.openinbrowser": "local_moodlemobileapp",
|
"core.openinbrowser": "local_moodlemobileapp",
|
||||||
|
"core.openmodinbrowser": "local_moodlemobileapp",
|
||||||
"core.othergroups": "group",
|
"core.othergroups": "group",
|
||||||
"core.pagea": "moodle",
|
"core.pagea": "moodle",
|
||||||
"core.parentlanguage": "langconfig",
|
"core.parentlanguage": "langconfig",
|
||||||
|
@ -1866,6 +1873,7 @@
|
||||||
"core.previous": "moodle",
|
"core.previous": "moodle",
|
||||||
"core.proceed": "moodle",
|
"core.proceed": "moodle",
|
||||||
"core.pulltorefresh": "local_moodlemobileapp",
|
"core.pulltorefresh": "local_moodlemobileapp",
|
||||||
|
"core.qrscanner": "local_moodlemobileapp",
|
||||||
"core.question.answer": "question",
|
"core.question.answer": "question",
|
||||||
"core.question.answersaved": "question",
|
"core.question.answersaved": "question",
|
||||||
"core.question.cannotdeterminestatus": "local_moodlemobileapp",
|
"core.question.cannotdeterminestatus": "local_moodlemobileapp",
|
||||||
|
@ -1908,6 +1916,7 @@
|
||||||
"core.retry": "local_moodlemobileapp",
|
"core.retry": "local_moodlemobileapp",
|
||||||
"core.save": "moodle",
|
"core.save": "moodle",
|
||||||
"core.savechanges": "assign",
|
"core.savechanges": "assign",
|
||||||
|
"core.scanqr": "local_moodlemobileapp",
|
||||||
"core.search": "moodle",
|
"core.search": "moodle",
|
||||||
"core.searching": "local_moodlemobileapp",
|
"core.searching": "local_moodlemobileapp",
|
||||||
"core.searchresults": "moodle",
|
"core.searchresults": "moodle",
|
||||||
|
|
|
@ -133,8 +133,15 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
|
||||||
ev && ev.stopPropagation();
|
ev && ev.stopPropagation();
|
||||||
|
|
||||||
if (this.assign && (this.description || this.assign.introattachments)) {
|
if (this.assign && (this.description || this.assign.introattachments)) {
|
||||||
this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component,
|
this.textUtils.viewText(this.translate.instant('core.description'), this.description, {
|
||||||
this.module.id, this.assign.introattachments, true, 'module', this.module.id, this.courseId);
|
component: this.component,
|
||||||
|
componentId: this.module.id,
|
||||||
|
files: this.assign.introattachments,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.module.id,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -678,8 +678,10 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
|
||||||
*/
|
*/
|
||||||
showAdvancedGrade(): void {
|
showAdvancedGrade(): void {
|
||||||
if (this.feedback && this.feedback.advancedgrade) {
|
if (this.feedback && this.feedback.advancedgrade) {
|
||||||
this.textUtils.expandText(this.translate.instant('core.grades.grade'), this.feedback.gradefordisplay,
|
this.textUtils.viewText(this.translate.instant('core.grades.grade'), this.feedback.gradefordisplay, {
|
||||||
AddonModAssignProvider.COMPONENT, this.moduleId);
|
component: AddonModAssignProvider.COMPONENT,
|
||||||
|
componentId: this.moduleId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,8 +65,14 @@ export class AddonModAssignFeedbackCommentsComponent extends AddonModAssignFeedb
|
||||||
|
|
||||||
if (this.text) {
|
if (this.text) {
|
||||||
// Open a new state with the text.
|
// Open a new state with the text.
|
||||||
this.textUtils.expandText(this.plugin.name, this.text, this.component, this.assign.cmid, undefined, true,
|
this.textUtils.viewText(this.plugin.name, this.text, {
|
||||||
'module', this.assign.cmid, this.assign.course);
|
component: this.component,
|
||||||
|
componentId: this.assign.cmid,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.assign.cmid,
|
||||||
|
courseId: this.assign.course,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (this.edit) {
|
} else if (this.edit) {
|
||||||
|
|
|
@ -83,8 +83,14 @@ export class AddonModAssignSubmissionOnlineTextComponent extends AddonModAssignS
|
||||||
|
|
||||||
if (text) {
|
if (text) {
|
||||||
// Open a new state with the interpolated contents.
|
// Open a new state with the interpolated contents.
|
||||||
this.textUtils.expandText(this.plugin.name, text, this.component, this.assign.cmid, undefined, true,
|
this.textUtils.viewText(this.plugin.name, text, {
|
||||||
'module', this.assign.cmid, this.assign.course);
|
component: this.component,
|
||||||
|
componentId: this.assign.cmid,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.assign.cmid,
|
||||||
|
courseId: this.assign.course,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -14,15 +14,12 @@
|
||||||
|
|
||||||
import { Component, Optional, Injector, Input } from '@angular/core';
|
import { Component, Optional, Injector, Input } from '@angular/core';
|
||||||
import { Content, ModalController } from 'ionic-angular';
|
import { Content, ModalController } from 'ionic-angular';
|
||||||
import { CoreAppProvider } from '@providers/app';
|
|
||||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
|
||||||
import {
|
import {
|
||||||
CoreCourseModuleMainResourceComponent, CoreCourseResourceDownloadResult
|
CoreCourseModuleMainResourceComponent, CoreCourseResourceDownloadResult
|
||||||
} from '@core/course/classes/main-resource-component';
|
} from '@core/course/classes/main-resource-component';
|
||||||
import {
|
import {
|
||||||
AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter, AddonModBookBook, AddonModBookNavStyle
|
AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter, AddonModBookBook, AddonModBookNavStyle
|
||||||
} from '../../providers/book';
|
} from '../../providers/book';
|
||||||
import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler';
|
|
||||||
import { CoreTagProvider } from '@core/tag/providers/tag';
|
import { CoreTagProvider } from '@core/tag/providers/tag';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { CoreLoggerProvider } from '@providers/logger';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
||||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
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 { CoreLoginHelperProvider } from '@core/login/providers/helper';
|
||||||
import { Keyboard } from '@ionic-native/keyboard';
|
import { Keyboard } from '@ionic-native/keyboard';
|
||||||
import { ScreenOrientation } from '@ionic-native/screen-orientation';
|
import { ScreenOrientation } from '@ionic-native/screen-orientation';
|
||||||
|
@ -140,7 +140,9 @@ export class MoodleMobileApp implements OnInit {
|
||||||
|
|
||||||
if (this.urlSchemesProvider.isCustomURL(url)) {
|
if (this.urlSchemesProvider.isCustomURL(url)) {
|
||||||
// Close the browser if it's a valid SSO 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);
|
this.utils.closeInAppBrowser(false);
|
||||||
|
|
||||||
} else if (this.platform.is('android')) {
|
} else if (this.platform.is('android')) {
|
||||||
|
@ -194,7 +196,9 @@ export class MoodleMobileApp implements OnInit {
|
||||||
this.lastUrls[url] = Date.now();
|
this.lastUrls[url] = Date.now();
|
||||||
|
|
||||||
this.eventsProvider.trigger(CoreEventsProvider.APP_LAUNCHED_URL, url);
|
this.eventsProvider.trigger(CoreEventsProvider.APP_LAUNCHED_URL, url);
|
||||||
this.urlSchemesProvider.handleCustomURL(url);
|
this.urlSchemesProvider.handleCustomURL(url).catch((error: CoreCustomURLSchemesHandleError) => {
|
||||||
|
this.urlSchemesProvider.treatHandleCustomURLError(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1275,3 +1275,17 @@ 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;
|
||||||
|
|
||||||
|
.ion-page {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
ion-content, ion-backdrop, ion-modal:not(.core-modal-fullscreen), core-ion-tabs {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
|
@ -1387,6 +1387,7 @@
|
||||||
"core.contentlinks.errorredirectothersite": "The redirect URL cannot point to a different site.",
|
"core.contentlinks.errorredirectothersite": "The redirect URL cannot point to a different site.",
|
||||||
"core.continue": "Continue",
|
"core.continue": "Continue",
|
||||||
"core.copiedtoclipboard": "Text copied to clipboard",
|
"core.copiedtoclipboard": "Text copied to clipboard",
|
||||||
|
"core.copytoclipboard": "Copy to clipboard",
|
||||||
"core.course": "Course",
|
"core.course": "Course",
|
||||||
"core.course.activitydisabled": "Your organisation has disabled this activity in the mobile app.",
|
"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.",
|
"core.course.activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.",
|
||||||
|
@ -1721,8 +1722,10 @@
|
||||||
"core.login.faqsetupsitequestion": "I want to set up my own Moodle site.",
|
"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 address\" field and click the \"Connect!\" button.",
|
"core.login.faqtestappanswer": "To test the app in a Moodle Demo Site, type \"teacher\" or \"student\" in the \"Your site address\" field and click the \"Connect!\" button.",
|
||||||
"core.login.faqtestappquestion": "I just want to test the app, what can I do?",
|
"core.login.faqtestappquestion": "I just want to test the app, what can I do?",
|
||||||
"core.login.faqwhatisurlanswer": "<p>Every organisation or school has their own custom address for their Moodle site.</p><p>To find the address of the Moodle site you want to connect to, do the following:</p><ol><li>Open a web browser and go to your school's or organisation's Moodle site login page</li><li>At the top of the page, on the address bar, you will see the URL of your Moodle site. E.g. \"campus.example.edu\".{{$image}}</li><li>Copy the address (do not copy the /login and what comes after), paste it into the Moodle App and click \"Connect!\"</li><li>Now you can log into your site, using your username and password</li>",
|
"core.login.faqwhatisurlanswer": "<p>Every organisation or school has their own custom address for their Moodle site.</p><p>To find the address of the Moodle site you want to connect to, do the following:</p><ol><li>Open a web browser and go to your school's or organisation's Moodle site login page</li><li>At the top of the page, on the address bar, you will see the URL of your Moodle site. E.g. \"campus.example.edu\".<br>{{$image}}</li><li>Copy the address (do not copy the /login and what comes after), paste it into the Moodle App and click \"Connect!\"</li><li>Now you can log into your site, using your username and password</li>",
|
||||||
"core.login.faqwhatisurlquestion": "What is the URL of my Moodle site? How can I find my school’s site?",
|
"core.login.faqwhatisurlquestion": "What is the URL of my Moodle site? How can I find my school’s site?",
|
||||||
|
"core.login.faqwhereisqrcode": "Where can I find the QR code?",
|
||||||
|
"core.login.faqwhereisqrcodeanswer": "<p>If your organisation has enabled it, you will find a QR code on the web site at the bottom of your user profile page.</p>{{$image}}",
|
||||||
"core.login.findyoursite": "Find your site",
|
"core.login.findyoursite": "Find your site",
|
||||||
"core.login.firsttime": "Is this your first time here?",
|
"core.login.firsttime": "Is this your first time here?",
|
||||||
"core.login.forcepasswordchangenotice": "You must change your password to proceed.",
|
"core.login.forcepasswordchangenotice": "You must change your password to proceed.",
|
||||||
|
@ -1752,6 +1755,7 @@
|
||||||
"core.login.mustconfirm": "You need to confirm your account",
|
"core.login.mustconfirm": "You need to confirm your account",
|
||||||
"core.login.newaccount": "New account",
|
"core.login.newaccount": "New account",
|
||||||
"core.login.notloggedin": "You need to be logged in.",
|
"core.login.notloggedin": "You need to be logged in.",
|
||||||
|
"core.login.or": "OR",
|
||||||
"core.login.password": "Password",
|
"core.login.password": "Password",
|
||||||
"core.login.passwordforgotten": "Forgotten 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.",
|
"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.",
|
||||||
|
@ -1789,6 +1793,7 @@
|
||||||
"core.login.usernotaddederror": "User not added - error",
|
"core.login.usernotaddederror": "User not added - error",
|
||||||
"core.login.visitchangepassword": "Do you want to visit the site to change the password?",
|
"core.login.visitchangepassword": "Do you want to visit the site to change the password?",
|
||||||
"core.login.webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.",
|
"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.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.lostconnection": "Your authentication token is invalid or has expired. You will have to reconnect to the site.",
|
||||||
"core.mainmenu.changesite": "Change site",
|
"core.mainmenu.changesite": "Change site",
|
||||||
|
@ -1868,6 +1873,7 @@
|
||||||
"core.previous": "Previous",
|
"core.previous": "Previous",
|
||||||
"core.proceed": "Proceed",
|
"core.proceed": "Proceed",
|
||||||
"core.pulltorefresh": "Pull to refresh",
|
"core.pulltorefresh": "Pull to refresh",
|
||||||
|
"core.qrscanner": "QR scanner",
|
||||||
"core.question.answer": "Answer",
|
"core.question.answer": "Answer",
|
||||||
"core.question.answersaved": "Answer saved",
|
"core.question.answersaved": "Answer saved",
|
||||||
"core.question.cannotdeterminestatus": "Cannot determine status",
|
"core.question.cannotdeterminestatus": "Cannot determine status",
|
||||||
|
@ -1910,6 +1916,7 @@
|
||||||
"core.retry": "Retry",
|
"core.retry": "Retry",
|
||||||
"core.save": "Save",
|
"core.save": "Save",
|
||||||
"core.savechanges": "Save changes",
|
"core.savechanges": "Save changes",
|
||||||
|
"core.scanqr": "Scan QR code",
|
||||||
"core.search": "Search",
|
"core.search": "Search",
|
||||||
"core.searching": "Searching",
|
"core.searching": "Searching",
|
||||||
"core.searchresults": "Search results",
|
"core.searchresults": "Search results",
|
||||||
|
|
|
@ -47,7 +47,13 @@ export class CoreNavigationBarComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
showInfo(): void {
|
showInfo(): void {
|
||||||
this.textUtils.expandText(this.title, this.info, this.component, this.componentId, [], true, this.contextLevel,
|
this.textUtils.viewText(this.title, this.info, {
|
||||||
this.contextInstanceId, this.courseId);
|
component: this.component,
|
||||||
|
componentId: this.componentId,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: this.contextLevel,
|
||||||
|
instanceId: this.contextInstanceId,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -265,8 +265,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
|
||||||
* Expand the description.
|
* Expand the description.
|
||||||
*/
|
*/
|
||||||
expandDescription(): void {
|
expandDescription(): void {
|
||||||
this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component, this.module.id,
|
this.textUtils.viewText(this.translate.instant('core.description'), this.description, {
|
||||||
[], true, 'module', this.module.id, this.courseId);
|
component: this.component,
|
||||||
|
componentId: this.module.id,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.module.id,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,7 +38,11 @@ export class CoreCourseUnsupportedModulePage {
|
||||||
* Expand the description.
|
* Expand the description.
|
||||||
*/
|
*/
|
||||||
expandDescription(): void {
|
expandDescription(): void {
|
||||||
this.textUtils.expandText(this.translate.instant('core.description'), this.module.description, undefined, undefined,
|
this.textUtils.viewText(this.translate.instant('core.description'), this.module.description, {
|
||||||
[], true, 'module', this.module.id, this.courseId);
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.module.id,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1269,14 +1269,17 @@ export class CoreCourseHelperProvider {
|
||||||
// Get the module.
|
// Get the module.
|
||||||
return this.courseProvider.getModule(moduleId, courseId, sectionId, false, false, siteId, modName);
|
return this.courseProvider.getModule(moduleId, courseId, sectionId, false, false, siteId, modName);
|
||||||
}).then((module) => {
|
}).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.
|
// 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.
|
// 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);
|
this.logger.warn('navCtrl was not passed to navigateToModule by the link handler for ' + module.modname);
|
||||||
|
|
|
@ -71,6 +71,11 @@
|
||||||
<core-icon name="fa-eraser"></core-icon>
|
<core-icon name="fa-eraser"></core-icon>
|
||||||
</button>
|
</button>
|
||||||
</ion-slide>
|
</ion-slide>
|
||||||
|
<ion-slide *ngIf="canScanQR">
|
||||||
|
<button [disabled]="!rteEnabled" (click)="scanQR($event)" (mousedown)="stopBubble($event)">
|
||||||
|
<core-icon name="fa-qrcode"></core-icon>
|
||||||
|
</button>
|
||||||
|
</ion-slide>
|
||||||
<ion-slide>
|
<ion-slide>
|
||||||
<button [attr.aria-pressed]="!rteEnabled" (click)="toggleEditor($event)" (mousedown)="mouseDownAction($event)" [title]=" 'core.editor.toggle' | translate">
|
<button [attr.aria-pressed]="!rteEnabled" (click)="toggleEditor($event)" (mousedown)="mouseDownAction($event)" [title]=" 'core.editor.toggle' | translate">
|
||||||
<core-icon name="fa-code"></core-icon>
|
<core-icon name="fa-code"></core-icon>
|
||||||
|
|
|
@ -95,6 +95,8 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
|
||||||
ol: 'false',
|
ol: 'false',
|
||||||
};
|
};
|
||||||
infoMessage: string;
|
infoMessage: string;
|
||||||
|
canScanQR: boolean;
|
||||||
|
|
||||||
protected isCurrentView = true;
|
protected isCurrentView = true;
|
||||||
protected toolbarButtonWidth = 40;
|
protected toolbarButtonWidth = 40;
|
||||||
protected toolbarArrowWidth = 28;
|
protected toolbarArrowWidth = 28;
|
||||||
|
@ -119,6 +121,7 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
|
||||||
this.contentChanged = new EventEmitter<string>();
|
this.contentChanged = new EventEmitter<string>();
|
||||||
this.element = elementRef.nativeElement as HTMLDivElement;
|
this.element = elementRef.nativeElement as HTMLDivElement;
|
||||||
this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp.
|
this.pageInstance = 'app_' + Date.now(); // Generate a "unique" ID based on timestamp.
|
||||||
|
this.canScanQR = this.utils.canScanQR();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -865,6 +868,24 @@ export class CoreEditorRichTextEditorComponent implements AfterContentInit, OnDe
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan a QR code and put its text in the editor.
|
||||||
|
*
|
||||||
|
* @param $event Event data
|
||||||
|
*/
|
||||||
|
scanQR($event: any): void {
|
||||||
|
this.stopBubble($event);
|
||||||
|
|
||||||
|
// Scan for a QR code.
|
||||||
|
this.utils.scanQR().then((text) => {
|
||||||
|
if (text) {
|
||||||
|
document.execCommand('insertText', false, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.content.resize(); // Resize content, otherwise the content height becomes 1 for some reason.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User entered the page that contains the component.
|
* User entered the page that contains the component.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { LocalNotifications } from '@ionic-native/local-notifications';
|
||||||
import { MediaCapture } from '@ionic-native/media-capture';
|
import { MediaCapture } from '@ionic-native/media-capture';
|
||||||
import { Network } from '@ionic-native/network';
|
import { Network } from '@ionic-native/network';
|
||||||
import { Push } from '@ionic-native/push';
|
import { Push } from '@ionic-native/push';
|
||||||
|
import { QRScanner } from '@ionic-native/qr-scanner';
|
||||||
import { SplashScreen } from '@ionic-native/splash-screen';
|
import { SplashScreen } from '@ionic-native/splash-screen';
|
||||||
import { StatusBar } from '@ionic-native/status-bar';
|
import { StatusBar } from '@ionic-native/status-bar';
|
||||||
import { SQLite } from '@ionic-native/sqlite';
|
import { SQLite } from '@ionic-native/sqlite';
|
||||||
|
@ -51,12 +52,14 @@ import { LocalNotificationsMock } from './providers/local-notifications';
|
||||||
import { MediaCaptureMock } from './providers/media-capture';
|
import { MediaCaptureMock } from './providers/media-capture';
|
||||||
import { NetworkMock } from './providers/network';
|
import { NetworkMock } from './providers/network';
|
||||||
import { PushMock } from './providers/push';
|
import { PushMock } from './providers/push';
|
||||||
|
import { QRScannerMock } from './providers/qr-scanner';
|
||||||
import { ZipMock } from './providers/zip';
|
import { ZipMock } from './providers/zip';
|
||||||
|
|
||||||
import { CoreEmulatorHelperProvider } from './providers/helper';
|
import { CoreEmulatorHelperProvider } from './providers/helper';
|
||||||
import { CoreEmulatorCaptureHelperProvider } from './providers/capture-helper';
|
import { CoreEmulatorCaptureHelperProvider } from './providers/capture-helper';
|
||||||
import { CoreAppProvider } from '@providers/app';
|
import { CoreAppProvider } from '@providers/app';
|
||||||
import { CoreFileProvider } from '@providers/file';
|
import { CoreFileProvider } from '@providers/file';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
|
import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
||||||
|
@ -80,6 +83,7 @@ export const IONIC_NATIVE_PROVIDERS = [
|
||||||
MediaCapture,
|
MediaCapture,
|
||||||
Network,
|
Network,
|
||||||
Push,
|
Push,
|
||||||
|
QRScanner,
|
||||||
SplashScreen,
|
SplashScreen,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
SQLite,
|
SQLite,
|
||||||
|
@ -206,6 +210,13 @@ export const IONIC_NATIVE_PROVIDERS = [
|
||||||
return appProvider.isMobile() ? new Push() : new PushMock(appProvider);
|
return appProvider.isMobile() ? new Push() : new PushMock(appProvider);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: QRScanner,
|
||||||
|
deps: [CoreAppProvider, CoreLoggerProvider],
|
||||||
|
useFactory: (appProvider: CoreAppProvider, loggerProvider: CoreLoggerProvider): QRScanner => {
|
||||||
|
return appProvider.isMobile() ? new QRScanner() : new QRScannerMock(loggerProvider);
|
||||||
|
}
|
||||||
|
},
|
||||||
SplashScreen,
|
SplashScreen,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
SQLite,
|
SQLite,
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
// (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 { QRScanner, QRScannerStatus } from '@ionic-native/qr-scanner';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { CoreLoggerProvider } from '@providers/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emulates the Cordova QR Scanner plugin in desktop apps and in browser.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class QRScannerMock extends QRScanner {
|
||||||
|
protected logger;
|
||||||
|
|
||||||
|
constructor(logger: CoreLoggerProvider) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.logger = logger.getInstance('QRScannerMock');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request permission to use QR scanner.
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
prepare(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this method to enable scanning. You must then call the `show` method to make the camera preview visible.
|
||||||
|
*
|
||||||
|
* @return Observable that emits the scanned text. Unsubscribe from the observable to stop scanning.
|
||||||
|
*/
|
||||||
|
scan(): Observable<string> {
|
||||||
|
this.logger.error('QRScanner isn\'t available in desktop apps.');
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the native webview to have a transparent background, then sets the background of the <body> and <html> DOM
|
||||||
|
* elements to transparent, allowing the webview to re-render with the transparent background.
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
show(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the native webview to be opaque with a white background, covering the video preview.
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
hide(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the device's light (for scanning in low-light environments).
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
enableLight(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy the scanner instance.
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
destroy(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the device's light.
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
disableLight(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use front camera
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
useFrontCamera(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use back camera
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
useBackCamera(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set camera to be used.
|
||||||
|
*
|
||||||
|
* @param camera Provide `0` for back camera, and `1` for front camera.
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
useCamera(camera: number): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses the video preview on the current frame and pauses scanning.
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
pausePreview(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resumse the video preview and resumes scanning.
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
resumePreview(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns permission status
|
||||||
|
*
|
||||||
|
* @return Promise.
|
||||||
|
*/
|
||||||
|
getStatus(): Promise<QRScannerStatus> {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens settings to edit app permissions.
|
||||||
|
*/
|
||||||
|
openSettings(): void {
|
||||||
|
this.logger.error('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,8 +33,10 @@
|
||||||
"faqsetupsitequestion": "I want to set up my own Moodle site.",
|
"faqsetupsitequestion": "I want to set up my own Moodle site.",
|
||||||
"faqtestappanswer": "To test the app in a Moodle Demo Site, type \"teacher\" or \"student\" in the \"Your site address\" field and click the \"Connect!\" button.",
|
"faqtestappanswer": "To test the app in a Moodle Demo Site, type \"teacher\" or \"student\" in the \"Your site address\" field and click the \"Connect!\" button.",
|
||||||
"faqtestappquestion": "I just want to test the app, what can I do?",
|
"faqtestappquestion": "I just want to test the app, what can I do?",
|
||||||
"faqwhatisurlanswer": "<p>Every organisation or school has their own custom address for their Moodle site.</p><p>To find the address of the Moodle site you want to connect to, do the following:</p><ol><li>Open a web browser and go to your school's or organisation's Moodle site login page</li><li>At the top of the page, on the address bar, you will see the URL of your Moodle site. E.g. \"campus.example.edu\".{{$image}}</li><li>Copy the address (do not copy the /login and what comes after), paste it into the Moodle App and click \"Connect!\"</li><li>Now you can log into your site, using your username and password</li>",
|
"faqwhatisurlanswer": "<p>Every organisation or school has their own custom address for their Moodle site.</p><p>To find the address of the Moodle site you want to connect to, do the following:</p><ol><li>Open a web browser and go to your school's or organisation's Moodle site login page</li><li>At the top of the page, on the address bar, you will see the URL of your Moodle site. E.g. \"campus.example.edu\".<br>{{$image}}</li><li>Copy the address (do not copy the /login and what comes after), paste it into the Moodle App and click \"Connect!\"</li><li>Now you can log into your site, using your username and password</li>",
|
||||||
"faqwhatisurlquestion": "What is the URL of my Moodle site? How can I find my school’s site?",
|
"faqwhatisurlquestion": "What is the URL of my Moodle site? How can I find my school’s site?",
|
||||||
|
"faqwhereisqrcode": "Where can I find the QR code?",
|
||||||
|
"faqwhereisqrcodeanswer": "<p>If your organisation has enabled it, you will find a QR code on the web site at the bottom of your user profile page.</p>{{$image}}",
|
||||||
"findyoursite": "Find your site",
|
"findyoursite": "Find your site",
|
||||||
"firsttime": "Is this your first time here?",
|
"firsttime": "Is this your first time here?",
|
||||||
"forcepasswordchangenotice": "You must change your password to proceed.",
|
"forcepasswordchangenotice": "You must change your password to proceed.",
|
||||||
|
@ -64,6 +66,7 @@
|
||||||
"mustconfirm": "You need to confirm your account",
|
"mustconfirm": "You need to confirm your account",
|
||||||
"newaccount": "New account",
|
"newaccount": "New account",
|
||||||
"notloggedin": "You need to be logged in.",
|
"notloggedin": "You need to be logged in.",
|
||||||
|
"or": "OR",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"passwordforgotten": "Forgotten password",
|
"passwordforgotten": "Forgotten password",
|
||||||
"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.",
|
"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.",
|
||||||
|
@ -100,6 +103,7 @@
|
||||||
"usernamerequired": "Username required",
|
"usernamerequired": "Username required",
|
||||||
"usernotaddederror": "User not added - error",
|
"usernotaddederror": "User not added - error",
|
||||||
"visitchangepassword": "Do you want to visit the site to change the password?",
|
"visitchangepassword": "Do you want to visit the site to change the password?",
|
||||||
"yourenteredsite": "Connect to your site",
|
"webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help.",
|
||||||
"webservicesnotenabled": "Your host site may not have enabled Web services. Please contact your administrator for help."
|
"youcanstillconnectwithcredentials": "You can still connect to the site by entering your username and password.",
|
||||||
|
"yourenteredsite": "Connect to your site"
|
||||||
}
|
}
|
|
@ -317,7 +317,7 @@ export class CoreLoginEmailSignupPage {
|
||||||
* Show authentication instructions.
|
* Show authentication instructions.
|
||||||
*/
|
*/
|
||||||
protected showAuthInstructions(): void {
|
protected showAuthInstructions(): void {
|
||||||
this.textUtils.expandText(this.translate.instant('core.login.instructions'), this.authInstructions);
|
this.textUtils.viewText(this.translate.instant('core.login.instructions'), this.authInstructions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<ion-item text-wrap>
|
<ion-item text-wrap>
|
||||||
<h2><b>{{ 'core.login.faqwhatisurlquestion' | translate }}</b></h2>
|
<h2><b>{{ 'core.login.faqwhatisurlquestion' | translate }}</b></h2>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item text-wrap>
|
<ion-item text-wrap class="core-login-faqwhatisurlanswer">
|
||||||
<div [innerHTML]="'core.login.faqwhatisurlanswer' | translate: {$image: urlImageHtml}">
|
<div [innerHTML]="'core.login.faqwhatisurlanswer' | translate: {$image: urlImageHtml}">
|
||||||
</div>
|
</div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
@ -36,5 +36,12 @@
|
||||||
<ion-item text-wrap>
|
<ion-item text-wrap>
|
||||||
<p>{{ 'core.login.faqtestappanswer' | translate }}</p>
|
<p>{{ 'core.login.faqtestappanswer' | translate }}</p>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="canScanQR">
|
||||||
|
<h2><b>{{ 'core.login.faqwhereisqrcode' | translate }}</b></h2>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item text-wrap *ngIf="canScanQR" class="core-login-faqwhereisqrcodeanswer">
|
||||||
|
<div [innerHTML]="'core.login.faqwhereisqrcodeanswer' | translate: {$image: qrCodeImageHtml}">
|
||||||
|
</div>
|
||||||
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -2,4 +2,14 @@ page-core-login-site-help {
|
||||||
.content {
|
.content {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.core-login-faqwhatisurlanswer img {
|
||||||
|
max-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-login-faqwhereisqrcodeanswer img {
|
||||||
|
max-height: 220px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -15,6 +15,8 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { IonicPage, ViewController } from 'ionic-angular';
|
import { IonicPage, ViewController } from 'ionic-angular';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
import { CoreLoginHelperProvider } from '../../providers/helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays some help regarding the CoreLoginSitePage.
|
* Component that displays some help regarding the CoreLoginSitePage.
|
||||||
|
@ -28,11 +30,16 @@ export class CoreLoginSiteHelpPage {
|
||||||
|
|
||||||
urlImageHtml: string;
|
urlImageHtml: string;
|
||||||
setupLinkHtml: string;
|
setupLinkHtml: string;
|
||||||
|
qrCodeImageHtml: string;
|
||||||
|
canScanQR: boolean;
|
||||||
|
|
||||||
constructor(protected viewCtrl: ViewController,
|
constructor(protected viewCtrl: ViewController,
|
||||||
protected translate: TranslateService) {
|
protected translate: TranslateService,
|
||||||
|
protected utils: CoreUtilsProvider) {
|
||||||
|
|
||||||
this.urlImageHtml = '<img src="assets/img/login/faq_url.png" role="presentation">';
|
this.canScanQR = this.utils.canScanQR() && false; // @todo: Enable it for 3.9 release.
|
||||||
|
this.urlImageHtml = CoreLoginHelperProvider.FAQ_URL_IMAGE_HTML;
|
||||||
|
this.qrCodeImageHtml = CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML;
|
||||||
this.setupLinkHtml = '<a href="https://moodle.com/getstarted/" title="' +
|
this.setupLinkHtml = '<a href="https://moodle.com/getstarted/" title="' +
|
||||||
this.translate.instant('core.login.faqsetupsitelinktitle') + '">https://moodle.com/getstarted/</a>';
|
this.translate.instant('core.login.faqsetupsitelinktitle') + '">https://moodle.com/getstarted/</a>';
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,17 @@
|
||||||
<ion-option *ngFor="let site of fixedSites" [value]="site.url">{{site.name}}</ion-option>
|
<ion-option *ngFor="let site of fixedSites" [value]="site.url">{{site.name}}</ion-option>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
|
||||||
|
<ng-container *ngIf="!fixedSites && showScanQR && !hasSites">
|
||||||
|
<div class="core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div>
|
||||||
|
<ion-item>
|
||||||
|
<a ion-button block color="light" margin-top icon-start (click)="scanQR()">
|
||||||
|
<core-icon name="fa-qrcode" aria-hidden="true"></core-icon>
|
||||||
|
{{ 'core.scanqr' | translate }}
|
||||||
|
</a>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Pick the site from a list of fixed sites. -->
|
<!-- Pick the site from a list of fixed sites. -->
|
||||||
|
|
|
@ -115,4 +115,10 @@ ion-app.app-root page-core-login-site {
|
||||||
.core-login-default-icon {
|
.core-login-default-icon {
|
||||||
filter: grayscale(100%);
|
filter: grayscale(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.core-login-site-qrcode-separator {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,11 @@ import { IonicPage, NavController, ModalController, AlertController, NavParams }
|
||||||
import { CoreAppProvider } from '@providers/app';
|
import { CoreAppProvider } from '@providers/app';
|
||||||
import { CoreEventsProvider } from '@providers/events';
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
import { CoreSitesProvider, CoreSiteCheckResponse, CoreLoginSiteInfo } from '@providers/sites';
|
import { CoreSitesProvider, CoreSiteCheckResponse, CoreLoginSiteInfo } from '@providers/sites';
|
||||||
|
import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes';
|
||||||
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
import { CoreConfigConstants } from '../../../../configconstants';
|
import { CoreConfigConstants } from '../../../../configconstants';
|
||||||
import { CoreLoginHelperProvider } from '../../providers/helper';
|
import { CoreLoginHelperProvider } from '../../providers/helper';
|
||||||
import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms';
|
import { FormBuilder, FormGroup, ValidatorFn, AbstractControl } from '@angular/forms';
|
||||||
|
@ -58,6 +60,7 @@ export class CoreLoginSitePage {
|
||||||
loadingSites = false;
|
loadingSites = false;
|
||||||
onlyWrittenSite = false;
|
onlyWrittenSite = false;
|
||||||
searchFnc: Function;
|
searchFnc: Function;
|
||||||
|
showScanQR: boolean;
|
||||||
|
|
||||||
constructor(navParams: NavParams,
|
constructor(navParams: NavParams,
|
||||||
protected navCtrl: NavController,
|
protected navCtrl: NavController,
|
||||||
|
@ -71,9 +74,12 @@ export class CoreLoginSitePage {
|
||||||
protected domUtils: CoreDomUtilsProvider,
|
protected domUtils: CoreDomUtilsProvider,
|
||||||
protected eventsProvider: CoreEventsProvider,
|
protected eventsProvider: CoreEventsProvider,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
protected utils: CoreUtilsProvider) {
|
protected utils: CoreUtilsProvider,
|
||||||
|
protected urlSchemesProvider: CoreCustomURLSchemesProvider,
|
||||||
|
protected textUtils: CoreTextUtilsProvider) {
|
||||||
|
|
||||||
this.showKeyboard = !!navParams.get('showKeyboard');
|
this.showKeyboard = !!navParams.get('showKeyboard');
|
||||||
|
this.showScanQR = this.utils.canScanQR();
|
||||||
|
|
||||||
let url = '';
|
let url = '';
|
||||||
|
|
||||||
|
@ -231,7 +237,7 @@ export class CoreLoginSitePage {
|
||||||
* Show a help modal.
|
* Show a help modal.
|
||||||
*/
|
*/
|
||||||
showHelp(): void {
|
showHelp(): void {
|
||||||
const modal = this.modalCtrl.create('CoreLoginSiteHelpPage');
|
const modal = this.modalCtrl.create('CoreLoginSiteHelpPage', {}, { cssClass: 'core-modal-fullscreen' });
|
||||||
modal.present();
|
modal.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,20 +346,115 @@ export class CoreLoginSitePage {
|
||||||
* @return {ValidatorFn} Validation results.
|
* @return {ValidatorFn} Validation results.
|
||||||
*/
|
*/
|
||||||
protected moodleUrlValidator(): ValidatorFn {
|
protected moodleUrlValidator(): ValidatorFn {
|
||||||
return (control: AbstractControl): {[key: string]: any} | null => {
|
return (control: AbstractControl): {[key: string]: any} | null => {
|
||||||
const value = control.value.trim();
|
const value = control.value.trim();
|
||||||
let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value);
|
let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
const demo = !!this.getDemoSiteData(value);
|
const demo = !!this.getDemoSiteData(value);
|
||||||
|
|
||||||
if (demo) {
|
if (demo) {
|
||||||
valid = true;
|
valid = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return valid ? null : {siteUrl: {value: control.value}};
|
return valid ? null : {siteUrl: {value: control.value}};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show instructions and scan QR code.
|
||||||
|
*
|
||||||
|
* @todo Use it in 3.9 release instead of scanQR.
|
||||||
|
*/
|
||||||
|
showInstructionsAndScanQR(): void {
|
||||||
|
// Show some instructions first.
|
||||||
|
this.domUtils.showAlertWithButtons(
|
||||||
|
this.translate.instant('core.login.faqwhereisqrcode'),
|
||||||
|
this.translate.instant('core.login.faqwhereisqrcodeanswer', {$image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML}),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: this.translate.instant('core.cancel'),
|
||||||
|
role: 'cancel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.translate.instant('core.next'),
|
||||||
|
handler: (): void => {
|
||||||
|
this.scanQR();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan a QR code and put its text in the URL input.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async scanQR(): Promise<void> {
|
||||||
|
// Scan for a QR code.
|
||||||
|
const text = await this.utils.scanQR();
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
if (this.urlSchemesProvider.isCustomURL(text)) {
|
||||||
|
try {
|
||||||
|
await this.urlSchemesProvider.handleCustomURL(text);
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.data && error.data.isAuthenticationURL && error.data.siteUrl) {
|
||||||
|
// An error ocurred, but it's an authentication URL and we have the site URL.
|
||||||
|
this.treatErrorInAuthenticationCustomURL(text, error);
|
||||||
|
} else {
|
||||||
|
this.urlSchemesProvider.treatHandleCustomURLError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a custom URL scheme, put the text in the field.
|
||||||
|
this.siteForm.controls.siteUrl.setValue(text);
|
||||||
|
|
||||||
|
this.connect(new Event('click'), text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Treat an error while handling a custom URL meant to perform an authentication.
|
||||||
|
* If the site doesn't use SSO, the user will be sent to the credentials screen.
|
||||||
|
*
|
||||||
|
* @param customURL Custom URL handled.
|
||||||
|
* @param error Error data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async treatErrorInAuthenticationCustomURL(customURL: string, error: CoreCustomURLSchemesHandleError): Promise<void> {
|
||||||
|
const siteUrl = error.data.siteUrl;
|
||||||
|
const modal = this.domUtils.showModalLoading();
|
||||||
|
|
||||||
|
// Set the site URL in the input.
|
||||||
|
this.siteForm.controls.siteUrl.setValue(siteUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if site uses SSO.
|
||||||
|
const response = await this.sitesProvider.checkSite(siteUrl);
|
||||||
|
|
||||||
|
await this.sitesProvider.checkRequiredMinimumVersion(response.config);
|
||||||
|
|
||||||
|
if (!this.loginHelper.isSSOLoginNeeded(response.code)) {
|
||||||
|
// No SSO, go to credentials page.
|
||||||
|
await this.navCtrl.push('CoreLoginCredentialsPage', {
|
||||||
|
siteUrl: response.siteUrl,
|
||||||
|
siteConfig: response.config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore errors.
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now display the error.
|
||||||
|
error.error = this.textUtils.addTextToError(error.error,
|
||||||
|
'<br><br>' + this.translate.instant('core.login.youcanstillconnectwithcredentials'));
|
||||||
|
|
||||||
|
this.urlSchemesProvider.treatHandleCustomURLError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,8 @@ export interface CoreLoginSSOData {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CoreLoginHelperProvider {
|
export class CoreLoginHelperProvider {
|
||||||
static OPEN_COURSE = 'open_course';
|
static OPEN_COURSE = 'open_course';
|
||||||
|
static FAQ_URL_IMAGE_HTML = '<img src="assets/img/login/faq_url.png" role="presentation">';
|
||||||
|
static FAQ_QRCODE_IMAGE_HTML = '<img src="assets/img/login/faq_qrcode.png" role="presentation">';
|
||||||
|
|
||||||
protected logger;
|
protected logger;
|
||||||
protected isSSOConfirmShown = false;
|
protected isSSOConfirmShown = false;
|
||||||
|
|
|
@ -31,6 +31,10 @@
|
||||||
<h2>{{item.label}}</h2>
|
<h2>{{item.label}}</h2>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<a ion-item *ngIf="showScanQR" (click)="scanQR()">
|
||||||
|
<core-icon name="fa-qrcode" item-start aria-hidden="true"></core-icon>
|
||||||
|
<h2>{{ 'core.scanqr' | translate }}</h2>
|
||||||
|
</a>
|
||||||
<a *ngIf="showWeb" ion-item [href]="siteInfo.siteurl" core-link autoLogin="yes" title="{{ 'core.mainmenu.website' | translate }}">
|
<a *ngIf="showWeb" ion-item [href]="siteInfo.siteurl" core-link autoLogin="yes" title="{{ 'core.mainmenu.website' | translate }}">
|
||||||
<ion-icon name="globe" item-start aria-hidden="true"></ion-icon>
|
<ion-icon name="globe" item-start aria-hidden="true"></ion-icon>
|
||||||
<h2>{{ 'core.mainmenu.website' | translate }}</h2>
|
<h2>{{ 'core.mainmenu.website' | translate }}</h2>
|
||||||
|
|
|
@ -16,9 +16,14 @@ import { Component, OnDestroy } from '@angular/core';
|
||||||
import { IonicPage, NavController } from 'ionic-angular';
|
import { IonicPage, NavController } from 'ionic-angular';
|
||||||
import { CoreEventsProvider } from '@providers/events';
|
import { CoreEventsProvider } from '@providers/events';
|
||||||
import { CoreSitesProvider } from '@providers/sites';
|
import { CoreSitesProvider } from '@providers/sites';
|
||||||
|
import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes';
|
||||||
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate';
|
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate';
|
||||||
import { CoreMainMenuProvider, CoreMainMenuCustomItem } from '../../providers/mainmenu';
|
import { CoreMainMenuProvider, CoreMainMenuCustomItem } from '../../providers/mainmenu';
|
||||||
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
|
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
|
||||||
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of main menu options that aren't in the tabs.
|
* Page that displays the list of main menu options that aren't in the tabs.
|
||||||
|
@ -35,6 +40,7 @@ export class CoreMainMenuMorePage implements OnDestroy {
|
||||||
siteInfo: any;
|
siteInfo: any;
|
||||||
siteName: string;
|
siteName: string;
|
||||||
logoutLabel: string;
|
logoutLabel: string;
|
||||||
|
showScanQR: boolean;
|
||||||
showWeb: boolean;
|
showWeb: boolean;
|
||||||
showHelp: boolean;
|
showHelp: boolean;
|
||||||
docsUrl: string;
|
docsUrl: string;
|
||||||
|
@ -45,14 +51,23 @@ export class CoreMainMenuMorePage implements OnDestroy {
|
||||||
protected langObserver;
|
protected langObserver;
|
||||||
protected updateSiteObserver;
|
protected updateSiteObserver;
|
||||||
|
|
||||||
constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider,
|
constructor(protected menuDelegate: CoreMainMenuDelegate,
|
||||||
private navCtrl: NavController, private mainMenuProvider: CoreMainMenuProvider,
|
protected sitesProvider: CoreSitesProvider,
|
||||||
eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider) {
|
protected navCtrl: NavController,
|
||||||
|
protected mainMenuProvider: CoreMainMenuProvider,
|
||||||
|
eventsProvider: CoreEventsProvider,
|
||||||
|
protected loginHelper: CoreLoginHelperProvider,
|
||||||
|
protected utils: CoreUtilsProvider,
|
||||||
|
protected linkHelper: CoreContentLinksHelperProvider,
|
||||||
|
protected textUtils: CoreTextUtilsProvider,
|
||||||
|
protected urlSchemesProvider: CoreCustomURLSchemesProvider,
|
||||||
|
protected translate: TranslateService) {
|
||||||
|
|
||||||
this.langObserver = eventsProvider.on(CoreEventsProvider.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this));
|
this.langObserver = eventsProvider.on(CoreEventsProvider.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this));
|
||||||
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.loadSiteInfo.bind(this),
|
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.loadSiteInfo.bind(this),
|
||||||
sitesProvider.getCurrentSiteId());
|
sitesProvider.getCurrentSiteId());
|
||||||
this.loadSiteInfo();
|
this.loadSiteInfo();
|
||||||
|
this.showScanQR = this.utils.canScanQR();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -155,6 +170,36 @@ export class CoreMainMenuMorePage implements OnDestroy {
|
||||||
this.navCtrl.push('CoreSitePreferencesPage');
|
this.navCtrl.push('CoreSitePreferencesPage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scan and treat a QR code.
|
||||||
|
*/
|
||||||
|
scanQR(): void {
|
||||||
|
// Scan for a QR code.
|
||||||
|
this.utils.scanQR().then((text) => {
|
||||||
|
if (text) {
|
||||||
|
if (this.urlSchemesProvider.isCustomURL(text)) {
|
||||||
|
// Is a custom URL scheme, handle it.
|
||||||
|
this.urlSchemesProvider.handleCustomURL(text).catch((error: CoreCustomURLSchemesHandleError) => {
|
||||||
|
this.urlSchemesProvider.treatHandleCustomURLError(error);
|
||||||
|
});
|
||||||
|
} else if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { // Check if it's a URL.
|
||||||
|
// Check if the app can handle the URL.
|
||||||
|
this.linkHelper.handleLink(text, undefined, this.navCtrl, true, true).then((treated) => {
|
||||||
|
if (!treated) {
|
||||||
|
// Can't handle it, open it in browser.
|
||||||
|
this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// It's not a URL, open it in a modal so the user can see it and copy it.
|
||||||
|
this.textUtils.viewText(this.translate.instant('core.qrscanner'), text, {
|
||||||
|
displayCopyButton: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout the user.
|
* Logout the user.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -709,8 +709,14 @@ export class CoreQuestionHelperProvider {
|
||||||
if (span) {
|
if (span) {
|
||||||
// There's a hidden feedback, show it when the icon is clicked.
|
// There's a hidden feedback, show it when the icon is clicked.
|
||||||
icon.addEventListener('click', (event) => {
|
icon.addEventListener('click', (event) => {
|
||||||
this.textUtils.expandText(title, span.innerHTML, component, componentId, [], true, contextLevel,
|
this.textUtils.viewText(title, span.innerHTML, {
|
||||||
contextInstanceId, courseId);
|
component: component,
|
||||||
|
componentId: componentId,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: contextLevel,
|
||||||
|
instanceId: contextInstanceId,
|
||||||
|
courseId: courseId,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -146,8 +146,14 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
|
||||||
* Expand the description.
|
* Expand the description.
|
||||||
*/
|
*/
|
||||||
expandDescription(): void {
|
expandDescription(): void {
|
||||||
this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component, this.module.id,
|
this.textUtils.viewText(this.translate.instant('core.description'), this.description, {
|
||||||
[], true, 'module', this.module.id, this.courseId);
|
component: this.component,
|
||||||
|
componentId: this.module.id,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.module.id,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-navbar core-back-button>
|
||||||
|
<ion-title>{{ title }}</ion-title>
|
||||||
|
|
||||||
|
<ion-buttons end>
|
||||||
|
<button ion-button icon-only (click)="cancel()" [attr.aria-label]="'core.close' | translate">
|
||||||
|
<ion-icon name="close"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-navbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,31 @@
|
||||||
|
// (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 { CoreViewerQRScannerPage } from './qr-scanner';
|
||||||
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreViewerQRScannerPage
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreDirectivesModule,
|
||||||
|
IonicPageModule.forChild(CoreViewerQRScannerPage),
|
||||||
|
TranslateModule.forChild()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class CoreViewerQRScannerPageModule {}
|
|
@ -0,0 +1,79 @@
|
||||||
|
// (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 { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
|
||||||
|
import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page to scan a QR code.
|
||||||
|
*/
|
||||||
|
@IonicPage({ segment: 'core-viewer-qr-scanner' })
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-viewer-qr-scanner',
|
||||||
|
templateUrl: 'qr-scanner.html',
|
||||||
|
})
|
||||||
|
export class CoreViewerQRScannerPage {
|
||||||
|
title: string; // Page title.
|
||||||
|
|
||||||
|
constructor(params: NavParams,
|
||||||
|
translate: TranslateService,
|
||||||
|
protected viewCtrl: ViewController,
|
||||||
|
protected domUtils: CoreDomUtilsProvider,
|
||||||
|
protected utils: CoreUtilsProvider) {
|
||||||
|
|
||||||
|
this.title = params.get('title') || translate.instant('core.scanqr');
|
||||||
|
|
||||||
|
this.utils.startScanQR().then((text) => {
|
||||||
|
// Text captured, return it.
|
||||||
|
text = typeof text == 'string' ? text.trim() : '';
|
||||||
|
|
||||||
|
this.closeModal(text);
|
||||||
|
}).catch((error) => {
|
||||||
|
if (!error.coreCanceled) {
|
||||||
|
// Show error and stop scanning.
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'An error occurred.');
|
||||||
|
this.utils.stopScanQR();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel scanning.
|
||||||
|
*/
|
||||||
|
cancel(): void {
|
||||||
|
this.utils.stopScanQR();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal.
|
||||||
|
*
|
||||||
|
* @param text The text to return (if any).
|
||||||
|
*/
|
||||||
|
closeModal(text?: string): void {
|
||||||
|
this.viewCtrl.dismiss(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View will leave.
|
||||||
|
*/
|
||||||
|
ionViewWillLeave(): void {
|
||||||
|
// If this code is reached and scan hasn't been stopped yet it means the user clicked the back button, cancel.
|
||||||
|
this.utils.stopScanQR();
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,3 +16,9 @@
|
||||||
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="componentId"></core-file>
|
<core-file *ngFor="let file of files" [file]="file" [component]="component" [componentId]="componentId"></core-file>
|
||||||
</ion-card>
|
</ion-card>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
<ion-footer color="light" *ngIf="displayCopyButton">
|
||||||
|
<button ion-button block color="light" icon-start (click)="copyText()">
|
||||||
|
<ion-icon name="copy" aria-hidden="true"></ion-icon>
|
||||||
|
{{ 'core.copytoclipboard' | translate }}
|
||||||
|
</button>
|
||||||
|
</ion-footer>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
ion-app.app-root page-core-viewer-text {
|
||||||
|
ion-footer {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
|
import { IonicPage, ViewController, NavParams } from 'ionic-angular';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreUtils } from '@providers/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page to render a certain text. If opened as a modal, it will have a button to close the modal.
|
* Page to render a certain text. If opened as a modal, it will have a button to close the modal.
|
||||||
|
@ -34,6 +35,7 @@ export class CoreViewerTextPage {
|
||||||
contextLevel: string; // The context level.
|
contextLevel: string; // The context level.
|
||||||
instanceId: number; // The instance ID related to the context.
|
instanceId: number; // The instance ID related to the context.
|
||||||
courseId: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
courseId: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
||||||
|
displayCopyButton: boolean; // Whether to display a button to copy the contents.
|
||||||
|
|
||||||
constructor(private viewCtrl: ViewController, params: NavParams, textUtils: CoreTextUtilsProvider) {
|
constructor(private viewCtrl: ViewController, params: NavParams, textUtils: CoreTextUtilsProvider) {
|
||||||
this.title = params.get('title');
|
this.title = params.get('title');
|
||||||
|
@ -45,6 +47,7 @@ export class CoreViewerTextPage {
|
||||||
this.contextLevel = params.get('contextLevel');
|
this.contextLevel = params.get('contextLevel');
|
||||||
this.instanceId = params.get('instanceId');
|
this.instanceId = params.get('instanceId');
|
||||||
this.courseId = params.get('courseId');
|
this.courseId = params.get('courseId');
|
||||||
|
this.displayCopyButton = !!params.get('displayCopyButton');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,4 +56,11 @@ export class CoreViewerTextPage {
|
||||||
closeModal(): void {
|
closeModal(): void {
|
||||||
this.viewCtrl.dismiss();
|
this.viewCtrl.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the text to clipboard.
|
||||||
|
*/
|
||||||
|
copyText(): void {
|
||||||
|
CoreUtils.instance.copyToClipboard(this.content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreFilterProvider, CoreFilterFilter, CoreFilterFormatTextOptions } from '@core/filter/providers/filter';
|
import { CoreFilterProvider, CoreFilterFilter, CoreFilterFormatTextOptions } from '@core/filter/providers/filter';
|
||||||
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
|
import { CoreFilterHelperProvider } from '@core/filter/providers/helper';
|
||||||
import { CoreFilterDelegate } from '@core/filter/providers/delegate';
|
import { CoreFilterDelegate } from '@core/filter/providers/delegate';
|
||||||
|
import { CoreCustomURLSchemesProvider } from '@providers/urlschemes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
|
* Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective
|
||||||
|
@ -87,13 +88,14 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
protected contentLinksHelper: CoreContentLinksHelperProvider,
|
protected contentLinksHelper: CoreContentLinksHelperProvider,
|
||||||
@Optional() protected navCtrl: NavController,
|
@Optional() protected navCtrl: NavController,
|
||||||
@Optional() protected content: Content, @Optional()
|
@Optional() protected content: Content, @Optional()
|
||||||
protected svComponent: CoreSplitViewComponent,
|
@Optional() protected svComponent: CoreSplitViewComponent,
|
||||||
protected iframeUtils: CoreIframeUtilsProvider,
|
protected iframeUtils: CoreIframeUtilsProvider,
|
||||||
protected eventsProvider: CoreEventsProvider,
|
protected eventsProvider: CoreEventsProvider,
|
||||||
protected filterProvider: CoreFilterProvider,
|
protected filterProvider: CoreFilterProvider,
|
||||||
protected filterHelper: CoreFilterHelperProvider,
|
protected filterHelper: CoreFilterHelperProvider,
|
||||||
protected filterDelegate: CoreFilterDelegate,
|
protected filterDelegate: CoreFilterDelegate,
|
||||||
protected viewContainerRef: ViewContainerRef,
|
protected viewContainerRef: ViewContainerRef,
|
||||||
|
protected urlSchemesProvider: CoreCustomURLSchemesProvider
|
||||||
) {
|
) {
|
||||||
|
|
||||||
this.element = element.nativeElement;
|
this.element = element.nativeElement;
|
||||||
|
@ -305,8 +307,14 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
// Open a new state with the contents.
|
// Open a new state with the contents.
|
||||||
const filter = typeof this.filter != 'undefined' ? this.utils.isTrueOrOne(this.filter) : undefined;
|
const filter = typeof this.filter != 'undefined' ? this.utils.isTrueOrOne(this.filter) : undefined;
|
||||||
|
|
||||||
this.textUtils.expandText(this.fullTitle || this.translate.instant('core.description'), this.text,
|
this.textUtils.viewText(this.fullTitle || this.translate.instant('core.description'), this.text, {
|
||||||
this.component, this.componentId, undefined, filter, this.contextLevel, this.contextInstanceId, this.courseId);
|
component: this.component,
|
||||||
|
componentId: this.componentId,
|
||||||
|
filter: filter,
|
||||||
|
contextLevel: this.contextLevel,
|
||||||
|
instanceId: this.contextInstanceId,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -467,7 +475,7 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
anchors.forEach((anchor) => {
|
anchors.forEach((anchor) => {
|
||||||
// Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually.
|
// Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually.
|
||||||
const linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils,
|
const linkDir = new CoreLinkDirective(anchor, this.domUtils, this.utils, this.sitesProvider, this.urlUtils,
|
||||||
this.contentLinksHelper, this.navCtrl, this.content, this.svComponent, this.textUtils);
|
this.contentLinksHelper, this.navCtrl, this.content, this.svComponent, this.textUtils, this.urlSchemesProvider);
|
||||||
linkDir.capture = true;
|
linkDir.capture = true;
|
||||||
linkDir.ngOnInit();
|
linkDir.ngOnInit();
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,9 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
|
||||||
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
import { CoreUrlUtilsProvider } from '@providers/utils/url';
|
||||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||||
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
|
||||||
import { CoreConfigConstants } from '../configconstants';
|
|
||||||
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||||
|
import { CoreCustomURLSchemesProvider, CoreCustomURLSchemesHandleError } from '@providers/urlschemes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directive to open a link in external browser.
|
* Directive to open a link in external browser.
|
||||||
|
@ -39,11 +39,17 @@ export class CoreLinkDirective implements OnInit {
|
||||||
|
|
||||||
protected element: HTMLElement;
|
protected element: HTMLElement;
|
||||||
|
|
||||||
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
|
constructor(element: ElementRef,
|
||||||
private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider,
|
protected domUtils: CoreDomUtilsProvider,
|
||||||
private contentLinksHelper: CoreContentLinksHelperProvider, @Optional() private navCtrl: NavController,
|
protected utils: CoreUtilsProvider,
|
||||||
@Optional() private content: Content, @Optional() private svComponent: CoreSplitViewComponent,
|
protected sitesProvider: CoreSitesProvider,
|
||||||
private textUtils: CoreTextUtilsProvider) {
|
protected urlUtils: CoreUrlUtilsProvider,
|
||||||
|
protected contentLinksHelper: CoreContentLinksHelperProvider,
|
||||||
|
@Optional() protected navCtrl: NavController,
|
||||||
|
@Optional() protected content: Content,
|
||||||
|
@Optional() protected svComponent: CoreSplitViewComponent,
|
||||||
|
protected textUtils: CoreTextUtilsProvider,
|
||||||
|
protected urlSchemesProvider: CoreCustomURLSchemesProvider) {
|
||||||
// This directive can be added dynamically. In that case, the first param is the anchor HTMLElement.
|
// This directive can be added dynamically. In that case, the first param is the anchor HTMLElement.
|
||||||
this.element = element.nativeElement || element;
|
this.element = element.nativeElement || element;
|
||||||
}
|
}
|
||||||
|
@ -90,7 +96,6 @@ export class CoreLinkDirective implements OnInit {
|
||||||
* @param href HREF to be opened.
|
* @param href HREF to be opened.
|
||||||
*/
|
*/
|
||||||
protected navigate(href: string): void {
|
protected navigate(href: string): void {
|
||||||
const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link=';
|
|
||||||
|
|
||||||
if (this.urlUtils.isLocalFileUrl(href)) {
|
if (this.urlUtils.isLocalFileUrl(href)) {
|
||||||
// We have a local file.
|
// We have a local file.
|
||||||
|
@ -107,10 +112,10 @@ export class CoreLinkDirective implements OnInit {
|
||||||
// Look for id or name.
|
// Look for id or name.
|
||||||
this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']');
|
this.domUtils.scrollToElementBySelector(this.content, '#' + href + ', [name=\'' + href + '\']');
|
||||||
}
|
}
|
||||||
} else if (href.indexOf(contentLinksScheme) === 0) {
|
} else if (this.urlSchemesProvider.isCustomURL(href)) {
|
||||||
// Link should be treated by Custom URL Scheme. Encode the right part, otherwise ':' is removed in iOS.
|
this.urlSchemesProvider.handleCustomURL(href).catch((error: CoreCustomURLSchemesHandleError) => {
|
||||||
href = contentLinksScheme + encodeURIComponent(href.replace(contentLinksScheme, ''));
|
this.urlSchemesProvider.treatHandleCustomURLError(error);
|
||||||
this.utils.openInBrowser(href);
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// It's an external link, we will open with browser. Check if we need to auto-login.
|
// It's an external link, we will open with browser. Check if we need to auto-login.
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
"contenteditingsynced": "The content you are editing has been synced.",
|
"contenteditingsynced": "The content you are editing has been synced.",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"copiedtoclipboard": "Text copied to clipboard",
|
"copiedtoclipboard": "Text copied to clipboard",
|
||||||
|
"copytoclipboard": "Copy to clipboard",
|
||||||
"course": "Course",
|
"course": "Course",
|
||||||
"coursedetails": "Course details",
|
"coursedetails": "Course details",
|
||||||
"coursenogroups": "You are not a member of any group of this course.",
|
"coursenogroups": "You are not a member of any group of this course.",
|
||||||
|
@ -209,6 +210,7 @@
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"proceed": "Proceed",
|
"proceed": "Proceed",
|
||||||
"pulltorefresh": "Pull to refresh",
|
"pulltorefresh": "Pull to refresh",
|
||||||
|
"qrscanner": "QR scanner",
|
||||||
"quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.",
|
"quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.",
|
||||||
"redirectingtosite": "You will be redirected to the site.",
|
"redirectingtosite": "You will be redirected to the site.",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
|
@ -223,6 +225,7 @@
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"savechanges": "Save changes",
|
"savechanges": "Save changes",
|
||||||
|
"scanqr": "Scan QR code",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"searching": "Searching",
|
"searching": "Searching",
|
||||||
"searchresults": "Search results",
|
"searchresults": "Search results",
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CoreAppProvider } from './app';
|
import { CoreAppProvider } from './app';
|
||||||
import { CoreInitDelegate } from './init';
|
import { CoreInitDelegate } from './init';
|
||||||
import { CoreLoggerProvider } from './logger';
|
import { CoreLoggerProvider } from './logger';
|
||||||
import { CoreSitesProvider } from './sites';
|
import { CoreSitesProvider, CoreSiteCheckResponse } from './sites';
|
||||||
import { CoreDomUtilsProvider } from './utils/dom';
|
import { CoreDomUtilsProvider } from './utils/dom';
|
||||||
import { CoreTextUtilsProvider } from './utils/text';
|
import { CoreTextUtilsProvider } from './utils/text';
|
||||||
import { CoreUrlUtilsProvider } from './utils/url';
|
import { CoreUrlUtilsProvider } from './utils/url';
|
||||||
|
@ -44,6 +44,16 @@ export interface CoreCustomURLSchemesParams extends CoreLoginSSOData {
|
||||||
* URL to open once authenticated.
|
* URL to open once authenticated.
|
||||||
*/
|
*/
|
||||||
redirect?: any;
|
redirect?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether it's an SSO token URL.
|
||||||
|
*/
|
||||||
|
isSSOToken?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the URL is meant to perform an authentication.
|
||||||
|
*/
|
||||||
|
isAuthenticationURL?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -54,30 +64,66 @@ export class CoreCustomURLSchemesProvider {
|
||||||
protected logger;
|
protected logger;
|
||||||
protected lastUrls = {};
|
protected lastUrls = {};
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider,
|
constructor(logger: CoreLoggerProvider,
|
||||||
private loginHelper: CoreLoginHelperProvider, private linksHelper: CoreContentLinksHelperProvider,
|
protected appProvider: CoreAppProvider,
|
||||||
private initDelegate: CoreInitDelegate, private domUtils: CoreDomUtilsProvider, private urlUtils: CoreUrlUtilsProvider,
|
protected utils: CoreUtilsProvider,
|
||||||
private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
|
protected loginHelper: CoreLoginHelperProvider,
|
||||||
private linksDelegate: CoreContentLinksDelegate, private translate: TranslateService,
|
protected linksHelper: CoreContentLinksHelperProvider,
|
||||||
private sitePluginsProvider: CoreSitePluginsProvider) {
|
protected initDelegate: CoreInitDelegate,
|
||||||
|
protected domUtils: CoreDomUtilsProvider,
|
||||||
|
protected urlUtils: CoreUrlUtilsProvider,
|
||||||
|
protected sitesProvider: CoreSitesProvider,
|
||||||
|
protected textUtils: CoreTextUtilsProvider,
|
||||||
|
protected linksDelegate: CoreContentLinksDelegate,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected sitePluginsProvider: CoreSitePluginsProvider) {
|
||||||
this.logger = logger.getInstance('CoreCustomURLSchemesProvider');
|
this.logger = logger.getInstance('CoreCustomURLSchemesProvider');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given some data of a custom URL with a token, create a site if it needs to be created.
|
||||||
|
*
|
||||||
|
* @param data URL data.
|
||||||
|
* @return Promise resolved with the site ID.
|
||||||
|
*/
|
||||||
|
protected async createSiteIfNeeded(data: CoreCustomURLSchemesParams): Promise<string> {
|
||||||
|
if (!data.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSite = this.sitesProvider.getCurrentSite();
|
||||||
|
|
||||||
|
if (!currentSite || currentSite.getToken() != data.token) {
|
||||||
|
// Token belongs to a different site, create it. It doesn't matter if it already exists.
|
||||||
|
|
||||||
|
if (!data.siteUrl.match(/^https?:\/\//)) {
|
||||||
|
// URL doesn't have a protocol and it's required to be able to create the site. Check which one to use.
|
||||||
|
const result = await this.sitesProvider.checkSite(data.siteUrl);
|
||||||
|
|
||||||
|
data.siteUrl = result.siteUrl;
|
||||||
|
|
||||||
|
await this.sitesProvider.checkRequiredMinimumVersion(result.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sitesProvider.newSite(data.siteUrl, data.token, data.privateToken, data.isSSOToken,
|
||||||
|
this.loginHelper.getOAuthIdFromParams(data.ssoUrlParams));
|
||||||
|
} else {
|
||||||
|
// Token belongs to current site, no need to create it.
|
||||||
|
return this.sitesProvider.getCurrentSiteId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle an URL received by custom URL scheme.
|
* Handle an URL received by custom URL scheme.
|
||||||
*
|
*
|
||||||
* @param url URL to treat.
|
* @param url URL to treat.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done. If rejected, the parameter is of type CoreCustomURLSchemesHandleError.
|
||||||
*/
|
*/
|
||||||
handleCustomURL(url: string): Promise<any> {
|
async handleCustomURL(url: string): Promise<void> {
|
||||||
if (!this.isCustomURL(url)) {
|
if (!this.isCustomURL(url)) {
|
||||||
return Promise.reject(null);
|
throw new CoreCustomURLSchemesHandleError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
let modal,
|
|
||||||
isSSOToken = false,
|
|
||||||
data: CoreCustomURLSchemesParams;
|
|
||||||
|
|
||||||
/* First check that this URL hasn't been treated a few seconds ago. The function that handles custom URL schemes already
|
/* First check that this URL hasn't been treated a few seconds ago. The function that handles custom URL schemes already
|
||||||
does this, but this function is called from other places so we need to handle it in here too. */
|
does this, but this function is called from other places so we need to handle it in here too. */
|
||||||
if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) {
|
if (this.lastUrls[url] && Date.now() - this.lastUrls[url] < 3000) {
|
||||||
|
@ -86,69 +132,49 @@ export class CoreCustomURLSchemesProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastUrls[url] = Date.now();
|
this.lastUrls[url] = Date.now();
|
||||||
|
url = this.textUtils.decodeURIComponent(url);
|
||||||
|
|
||||||
// Wait for app to be ready.
|
// Wait for app to be ready.
|
||||||
return this.initDelegate.ready().then(() => {
|
await this.initDelegate.ready();
|
||||||
url = this.textUtils.decodeURIComponent(url);
|
|
||||||
|
|
||||||
// Some platforms like Windows add a slash at the end. Remove it.
|
// Some platforms like Windows add a slash at the end. Remove it.
|
||||||
// Some sites add a # at the end of the URL. If it's there, remove it.
|
// Some sites add a # at the end of the URL. If it's there, remove it.
|
||||||
url = url.replace(/\/?#?\/?$/, '');
|
url = url.replace(/\/?#?\/?$/, '');
|
||||||
|
|
||||||
modal = this.domUtils.showModalLoading();
|
const modal = this.domUtils.showModalLoading();
|
||||||
|
let data: CoreCustomURLSchemesParams;
|
||||||
|
|
||||||
// Get the data from the URL.
|
// Get the data from the URL.
|
||||||
|
try {
|
||||||
if (this.isCustomURLToken(url)) {
|
if (this.isCustomURLToken(url)) {
|
||||||
isSSOToken = true;
|
data = await this.getCustomURLTokenData(url);
|
||||||
|
|
||||||
return this.getCustomURLTokenData(url);
|
|
||||||
} else if (this.isCustomURLLink(url)) {
|
} else if (this.isCustomURLLink(url)) {
|
||||||
// In iOS, the protocol after the scheme doesn't have ":". Add it.
|
// In iOS, the protocol after the scheme doesn't have ":". Add it.
|
||||||
url = url.replace(/\/\/link=(https?)\/\//, '//link=$1://');
|
url = url.replace(/\/\/link=(https?)\/\//, '//link=$1://');
|
||||||
|
|
||||||
return this.getCustomURLLinkData(url);
|
data = await this.getCustomURLLinkData(url);
|
||||||
} else {
|
} else {
|
||||||
// In iOS, the protocol after the scheme doesn't have ":". Add it.
|
// In iOS, the protocol after the scheme doesn't have ":". Add it.
|
||||||
url = url.replace(/\/\/(https?)\/\//, '//$1://');
|
url = url.replace(/\/\/(https?)\/\//, '//$1://');
|
||||||
|
|
||||||
return this.getCustomURLData(url);
|
data = await this.getCustomURLData(url);
|
||||||
}
|
}
|
||||||
}).then((result) => {
|
} catch (error) {
|
||||||
data = result;
|
modal.dismiss();
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) {
|
if (data.redirect && data.redirect.match(/^https?:\/\//) && data.redirect.indexOf(data.siteUrl) == -1) {
|
||||||
// Redirect URL must belong to the same site. Reject.
|
// Redirect URL must belong to the same site. Reject.
|
||||||
return Promise.reject(this.translate.instant('core.contentlinks.errorredirectothersite'));
|
throw this.translate.instant('core.contentlinks.errorredirectothersite');
|
||||||
}
|
}
|
||||||
|
|
||||||
// First of all, authenticate the user if needed.
|
// First of all, create the site if needed.
|
||||||
const currentSite = this.sitesProvider.getCurrentSite();
|
const siteId = await this.createSiteIfNeeded(data);
|
||||||
|
|
||||||
if (data.token) {
|
if (data.isSSOToken) {
|
||||||
if (!currentSite || currentSite.getToken() != data.token) {
|
|
||||||
// Token belongs to a different site, create it. It doesn't matter if it already exists.
|
|
||||||
let promise;
|
|
||||||
|
|
||||||
if (!data.siteUrl.match(/^https?:\/\//)) {
|
|
||||||
// URL doesn't have a protocol and it's required to be able to create the site. Check which one to use.
|
|
||||||
promise = this.sitesProvider.checkSite(data.siteUrl).then((result) => {
|
|
||||||
data.siteUrl = result.siteUrl;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
promise = Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.then(() => {
|
|
||||||
return this.sitesProvider.newSite(data.siteUrl, data.token, data.privateToken, isSSOToken,
|
|
||||||
this.loginHelper.getOAuthIdFromParams(data.ssoUrlParams));
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Token belongs to current site, no need to create it.
|
|
||||||
return this.sitesProvider.getCurrentSiteId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).then((siteId) => {
|
|
||||||
if (isSSOToken) {
|
|
||||||
// Site created and authenticated, open the page to go.
|
// Site created and authenticated, open the page to go.
|
||||||
if (data.pageName) {
|
if (data.pageName) {
|
||||||
// State defined, go to that state instead of site initial page.
|
// State defined, go to that state instead of site initial page.
|
||||||
|
@ -165,113 +191,58 @@ export class CoreCustomURLSchemesProvider {
|
||||||
data.redirect = this.textUtils.concatenatePaths(data.siteUrl, data.redirect);
|
data.redirect = this.textUtils.concatenatePaths(data.siteUrl, data.redirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise;
|
let siteIds = [siteId];
|
||||||
|
|
||||||
if (siteId) {
|
if (!siteId) {
|
||||||
// Site created, we know the site to use.
|
// No site created, check if the site is stored (to know which one to use).
|
||||||
promise = Promise.resolve([siteId]);
|
siteIds = await this.sitesProvider.getSiteIdsFromUrl(data.siteUrl, true, data.username);
|
||||||
} else {
|
|
||||||
// Check if the site is stored.
|
|
||||||
promise = this.sitesProvider.getSiteIdsFromUrl(data.siteUrl, true, data.username);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then((siteIds) => {
|
if (siteIds.length > 1) {
|
||||||
if (siteIds.length > 1) {
|
// More than one site to treat the URL, let the user choose.
|
||||||
// More than one site to treat the URL, let the user choose.
|
this.linksHelper.goToChooseSite(data.redirect || data.siteUrl);
|
||||||
this.linksHelper.goToChooseSite(data.redirect || data.siteUrl);
|
|
||||||
|
|
||||||
} else if (siteIds.length == 1) {
|
} else if (siteIds.length == 1) {
|
||||||
// Only one site, handle the link.
|
// Only one site, handle the link.
|
||||||
return this.sitesProvider.getSite(siteIds[0]).then((site) => {
|
const site = await this.sitesProvider.getSite(siteIds[0]);
|
||||||
if (!data.redirect) {
|
|
||||||
// No redirect, go to the root URL if needed.
|
|
||||||
|
|
||||||
return this.linksHelper.handleRootURL(site, false, true);
|
|
||||||
} else {
|
|
||||||
// Handle the redirect link.
|
|
||||||
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
|
|
||||||
|
|
||||||
/* Always use the username from the site in this case. If the link has a username and a token,
|
|
||||||
this will make sure that the link is opened with the user the token belongs to. */
|
|
||||||
const username = site.getInfo().username || data.username;
|
|
||||||
|
|
||||||
return this.linksHelper.handleLink(data.redirect, username).then((treated) => {
|
|
||||||
if (!treated) {
|
|
||||||
this.domUtils.showErrorModal('core.contentlinks.errornoactions', true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (!data.redirect) {
|
||||||
|
// No redirect, go to the root URL if needed.
|
||||||
|
await this.linksHelper.handleRootURL(site, false, true);
|
||||||
} else {
|
} else {
|
||||||
// Site not stored. Try to add the site.
|
// Handle the redirect link.
|
||||||
return this.sitesProvider.checkSite(data.siteUrl).then((result) => {
|
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
|
||||||
// Site exists. We'll allow to add it.
|
|
||||||
const ssoNeeded = this.loginHelper.isSSOLoginNeeded(result.code),
|
|
||||||
pageName = 'CoreLoginCredentialsPage',
|
|
||||||
pageParams = {
|
|
||||||
siteUrl: result.siteUrl,
|
|
||||||
username: data.username,
|
|
||||||
urlToOpen: data.redirect,
|
|
||||||
siteConfig: result.config
|
|
||||||
};
|
|
||||||
let promise,
|
|
||||||
hasSitePluginsLoaded = false;
|
|
||||||
|
|
||||||
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
|
/* Always use the username from the site in this case. If the link has a username and a token,
|
||||||
|
this will make sure that the link is opened with the user the token belongs to. */
|
||||||
|
const username = site.getInfo().username || data.username;
|
||||||
|
|
||||||
if (!this.sitesProvider.isLoggedIn()) {
|
const treated = await this.linksHelper.handleLink(data.redirect, username);
|
||||||
// Not logged in, no need to confirm. If SSO the confirm will be shown later.
|
|
||||||
promise = Promise.resolve();
|
|
||||||
} else {
|
|
||||||
// Ask the user before changing site.
|
|
||||||
const confirmMsg = this.translate.instant('core.contentlinks.confirmurlothersite');
|
|
||||||
promise = this.domUtils.showConfirm(confirmMsg).then(() => {
|
|
||||||
if (!ssoNeeded) {
|
|
||||||
hasSitePluginsLoaded = this.sitePluginsProvider.hasSitePluginsLoaded;
|
|
||||||
if (hasSitePluginsLoaded) {
|
|
||||||
// Store the redirect since logout will restart the app.
|
|
||||||
this.appProvider.storeRedirect(CoreConstants.NO_SITE_ID, pageName, pageParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.sitesProvider.logout().catch(() => {
|
if (!treated) {
|
||||||
// Ignore errors (shouldn't happen).
|
this.domUtils.showErrorModal('core.contentlinks.errornoactions', true);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.then(() => {
|
|
||||||
if (ssoNeeded) {
|
|
||||||
this.loginHelper.confirmAndOpenBrowserForSSOLogin(
|
|
||||||
result.siteUrl, result.code, result.service, result.config && result.config.launchurl);
|
|
||||||
} else if (!hasSitePluginsLoaded) {
|
|
||||||
return this.loginHelper.goToNoSitePage(undefined, pageName, pageParams);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
}).catch((error) => {
|
|
||||||
if (error == 'Duplicated') {
|
|
||||||
// Duplicated request
|
|
||||||
} else if (error && isSSOToken) {
|
|
||||||
// An error occurred, display the error and logout the user.
|
|
||||||
this.loginHelper.treatUserTokenError(data.siteUrl, error);
|
|
||||||
this.sitesProvider.logout();
|
|
||||||
} else {
|
} else {
|
||||||
this.domUtils.showErrorModalDefault(error, this.translate.instant('core.login.invalidsite'));
|
// Site not stored. Try to add the site.
|
||||||
|
const result = await this.sitesProvider.checkSite(data.siteUrl);
|
||||||
|
|
||||||
|
// Site exists. We'll allow to add it.
|
||||||
|
modal.dismiss(); // Dismiss modal so it doesn't collide with confirms.
|
||||||
|
|
||||||
|
await this.goToAddSite(data, result);
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
|
||||||
|
} catch (error) {
|
||||||
|
throw new CoreCustomURLSchemesHandleError(error, data);
|
||||||
|
} finally {
|
||||||
modal.dismiss();
|
modal.dismiss();
|
||||||
|
|
||||||
if (isSSOToken) {
|
if (data.isSSOToken) {
|
||||||
this.appProvider.finishSSOAuthentication();
|
this.appProvider.finishSSOAuthentication();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -281,17 +252,16 @@ export class CoreCustomURLSchemesProvider {
|
||||||
* @param url URL to treat.
|
* @param url URL to treat.
|
||||||
* @return Promise resolved with the data.
|
* @return Promise resolved with the data.
|
||||||
*/
|
*/
|
||||||
protected getCustomURLData(url: string): Promise<CoreCustomURLSchemesParams> {
|
protected async getCustomURLData(url: string): Promise<CoreCustomURLSchemesParams> {
|
||||||
const urlScheme = CoreConfigConstants.customurlscheme + '://';
|
if (!this.isCustomURL(url)) {
|
||||||
if (url.indexOf(urlScheme) == -1) {
|
throw new CoreCustomURLSchemesHandleError(null);
|
||||||
return Promise.reject(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// App opened using custom URL scheme.
|
// App opened using custom URL scheme.
|
||||||
this.logger.debug('Treating custom URL scheme: ' + url);
|
this.logger.debug('Treating custom URL scheme: ' + url);
|
||||||
|
|
||||||
// Delete the sso scheme from the URL.
|
// Delete the sso scheme from the URL.
|
||||||
url = url.replace(urlScheme, '');
|
url = this.removeCustomURLScheme(url);
|
||||||
|
|
||||||
// Detect if there's a user specified.
|
// Detect if there's a user specified.
|
||||||
const username = this.urlUtils.getUsernameFromUrl(url);
|
const username = this.urlUtils.getUsernameFromUrl(url);
|
||||||
|
@ -307,34 +277,26 @@ export class CoreCustomURLSchemesProvider {
|
||||||
url = url.substr(0, url.indexOf('?'));
|
url = url.substr(0, url.indexOf('?'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise;
|
|
||||||
|
|
||||||
if (!url.match(/https?:\/\//)) {
|
if (!url.match(/https?:\/\//)) {
|
||||||
// Url doesn't have a protocol. Check if the site is stored in the app to be able to determine the protocol.
|
// Url doesn't have a protocol. Check if the site is stored in the app to be able to determine the protocol.
|
||||||
promise = this.sitesProvider.getSiteIdsFromUrl(url, true, username).then((siteIds) => {
|
const siteIds = await this.sitesProvider.getSiteIdsFromUrl(url, true, username);
|
||||||
if (siteIds.length) {
|
|
||||||
// There is at least 1 site with this URL. Use it to know the full URL.
|
if (siteIds.length) {
|
||||||
return this.sitesProvider.getSite(siteIds[0]).then((site) => {
|
// There is at least 1 site with this URL. Use it to know the full URL.
|
||||||
return site.getURL();
|
const site = await this.sitesProvider.getSite(siteIds[0]);
|
||||||
});
|
|
||||||
} else {
|
url = site.getURL();
|
||||||
// No site stored with this URL, just use the URL as it is.
|
}
|
||||||
return url;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
promise = Promise.resolve(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return promise.then((url) => {
|
return {
|
||||||
return {
|
siteUrl: url,
|
||||||
siteUrl: url,
|
username: username,
|
||||||
username: username,
|
token: params.token,
|
||||||
token: params.token,
|
privateToken: params.privateToken,
|
||||||
privateToken: params.privateToken,
|
redirect: params.redirect,
|
||||||
redirect: params.redirect
|
isAuthenticationURL: !!params.token,
|
||||||
};
|
};
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -343,17 +305,16 @@ export class CoreCustomURLSchemesProvider {
|
||||||
* @param url URL to treat.
|
* @param url URL to treat.
|
||||||
* @return Promise resolved with the data.
|
* @return Promise resolved with the data.
|
||||||
*/
|
*/
|
||||||
protected getCustomURLLinkData(url: string): Promise<CoreCustomURLSchemesParams> {
|
protected async getCustomURLLinkData(url: string): Promise<CoreCustomURLSchemesParams> {
|
||||||
const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link=';
|
if (!this.isCustomURLLink(url)) {
|
||||||
if (url.indexOf(contentLinksScheme) == -1) {
|
throw new CoreCustomURLSchemesHandleError(null);
|
||||||
return Promise.reject(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// App opened using custom URL scheme.
|
// App opened using custom URL scheme.
|
||||||
this.logger.debug('Treating custom URL scheme with link param: ' + url);
|
this.logger.debug('Treating custom URL scheme with link param: ' + url);
|
||||||
|
|
||||||
// Delete the sso scheme from the URL.
|
// Delete the sso scheme from the URL.
|
||||||
url = url.replace(contentLinksScheme, '');
|
url = this.removeCustomURLLinkScheme(url);
|
||||||
|
|
||||||
// Detect if there's a user specified.
|
// Detect if there's a user specified.
|
||||||
const username = this.urlUtils.getUsernameFromUrl(url);
|
const username = this.urlUtils.getUsernameFromUrl(url);
|
||||||
|
@ -362,43 +323,42 @@ export class CoreCustomURLSchemesProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// First of all, check if it's the root URL of a site.
|
// First of all, check if it's the root URL of a site.
|
||||||
return this.sitesProvider.isStoredRootURL(url, username).then((data): any => {
|
const data = await this.sitesProvider.isStoredRootURL(url, username);
|
||||||
|
|
||||||
if (data.site) {
|
if (data.site) {
|
||||||
// Root URL.
|
// Root URL.
|
||||||
return {
|
return {
|
||||||
siteUrl: data.site.getURL(),
|
siteUrl: data.site.getURL(),
|
||||||
username: username
|
username: username
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (data.siteIds.length > 0) {
|
} else if (data.siteIds.length > 0) {
|
||||||
// Not the root URL, but at least 1 site supports the URL. Get the site URL from the list of sites.
|
// Not the root URL, but at least 1 site supports the URL. Get the site URL from the list of sites.
|
||||||
return this.sitesProvider.getSite(data.siteIds[0]).then((site) => {
|
const site = await this.sitesProvider.getSite(data.siteIds[0]);
|
||||||
return {
|
|
||||||
siteUrl: site.getURL(),
|
|
||||||
username: username,
|
|
||||||
redirect: url
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
return {
|
||||||
// Get the site URL.
|
siteUrl: site.getURL(),
|
||||||
let siteUrl = this.linksDelegate.getSiteUrl(url),
|
username: username,
|
||||||
redirect = url;
|
redirect: url
|
||||||
|
};
|
||||||
|
|
||||||
if (!siteUrl) {
|
} else {
|
||||||
// Site URL not found, use the original URL since it could be the root URL of the site.
|
// Get the site URL.
|
||||||
siteUrl = url;
|
let siteUrl = this.linksDelegate.getSiteUrl(url);
|
||||||
redirect = undefined;
|
let redirect = url;
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
if (!siteUrl) {
|
||||||
siteUrl: siteUrl,
|
// Site URL not found, use the original URL since it could be the root URL of the site.
|
||||||
username: username,
|
siteUrl = url;
|
||||||
redirect: redirect
|
redirect = undefined;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return {
|
||||||
|
siteUrl: siteUrl,
|
||||||
|
username: username,
|
||||||
|
redirect: redirect
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -407,15 +367,14 @@ export class CoreCustomURLSchemesProvider {
|
||||||
* @param url URL to treat.
|
* @param url URL to treat.
|
||||||
* @return Promise resolved with the data.
|
* @return Promise resolved with the data.
|
||||||
*/
|
*/
|
||||||
protected getCustomURLTokenData(url: string): Promise<CoreCustomURLSchemesParams> {
|
protected async getCustomURLTokenData(url: string): Promise<CoreCustomURLSchemesParams> {
|
||||||
const ssoScheme = CoreConfigConstants.customurlscheme + '://token=';
|
if (!this.isCustomURLToken(url)) {
|
||||||
if (url.indexOf(ssoScheme) == -1) {
|
throw new CoreCustomURLSchemesHandleError(null);
|
||||||
return Promise.reject(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.appProvider.isSSOAuthenticationOngoing()) {
|
if (this.appProvider.isSSOAuthenticationOngoing()) {
|
||||||
// Authentication ongoing, probably duplicated request.
|
// Authentication ongoing, probably duplicated request.
|
||||||
return Promise.reject('Duplicated');
|
throw new CoreCustomURLSchemesHandleError('Duplicated');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.appProvider.isDesktop()) {
|
if (this.appProvider.isDesktop()) {
|
||||||
|
@ -428,7 +387,7 @@ export class CoreCustomURLSchemesProvider {
|
||||||
this.logger.debug('App launched by URL with an SSO');
|
this.logger.debug('App launched by URL with an SSO');
|
||||||
|
|
||||||
// Delete the sso scheme from the URL.
|
// Delete the sso scheme from the URL.
|
||||||
url = url.replace(ssoScheme, '');
|
url = this.removeCustomURLTokenScheme(url);
|
||||||
|
|
||||||
// Some platforms like Windows add a slash at the end. Remove it.
|
// Some platforms like Windows add a slash at the end. Remove it.
|
||||||
// Some sites add a # at the end of the URL. If it's there, remove it.
|
// Some sites add a # at the end of the URL. If it's there, remove it.
|
||||||
|
@ -441,10 +400,56 @@ export class CoreCustomURLSchemesProvider {
|
||||||
// Error decoding the parameter.
|
// Error decoding the parameter.
|
||||||
this.logger.error('Error decoding parameter received for login SSO');
|
this.logger.error('Error decoding parameter received for login SSO');
|
||||||
|
|
||||||
return Promise.reject(null);
|
throw new CoreCustomURLSchemesHandleError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.loginHelper.validateBrowserSSOLogin(url);
|
const data: CoreCustomURLSchemesParams = await this.loginHelper.validateBrowserSSOLogin(url);
|
||||||
|
|
||||||
|
data.isSSOToken = true;
|
||||||
|
data.isAuthenticationURL = true;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to page to add a site, or open a browser if SSO.
|
||||||
|
*
|
||||||
|
* @param data URL data.
|
||||||
|
* @param checkResponse Result of checkSite.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async goToAddSite(data: CoreCustomURLSchemesParams, checkResponse: CoreSiteCheckResponse): Promise<void> {
|
||||||
|
const ssoNeeded = this.loginHelper.isSSOLoginNeeded(checkResponse.code);
|
||||||
|
const pageName = 'CoreLoginCredentialsPage';
|
||||||
|
const pageParams = {
|
||||||
|
siteUrl: checkResponse.siteUrl,
|
||||||
|
username: data.username,
|
||||||
|
urlToOpen: data.redirect,
|
||||||
|
siteConfig: checkResponse.config
|
||||||
|
};
|
||||||
|
let hasSitePluginsLoaded = false;
|
||||||
|
|
||||||
|
if (this.sitesProvider.isLoggedIn()) {
|
||||||
|
// Ask the user before changing site.
|
||||||
|
await this.domUtils.showConfirm(this.translate.instant('core.contentlinks.confirmurlothersite'));
|
||||||
|
|
||||||
|
if (!ssoNeeded) {
|
||||||
|
hasSitePluginsLoaded = this.sitePluginsProvider.hasSitePluginsLoaded;
|
||||||
|
if (hasSitePluginsLoaded) {
|
||||||
|
// Store the redirect since logout will restart the app.
|
||||||
|
this.appProvider.storeRedirect(CoreConstants.NO_SITE_ID, pageName, pageParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sitesProvider.logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ssoNeeded) {
|
||||||
|
this.loginHelper.confirmAndOpenBrowserForSSOLogin(checkResponse.siteUrl, checkResponse.code, checkResponse.service,
|
||||||
|
checkResponse.config && checkResponse.config.launchurl);
|
||||||
|
} else if (!hasSitePluginsLoaded) {
|
||||||
|
await this.loginHelper.goToNoSitePage(undefined, pageName, pageParams);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -488,6 +493,67 @@ export class CoreCustomURLSchemesProvider {
|
||||||
|
|
||||||
return url.indexOf(CoreConfigConstants.customurlscheme + '://token=') != -1;
|
return url.indexOf(CoreConfigConstants.customurlscheme + '://token=') != -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the scheme from a custom URL.
|
||||||
|
*
|
||||||
|
* @param url URL to treat.
|
||||||
|
* @return URL without scheme.
|
||||||
|
*/
|
||||||
|
removeCustomURLScheme(url: string): string {
|
||||||
|
return url.replace(CoreConfigConstants.customurlscheme + '://', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the scheme and the "link=" prefix from a link custom URL.
|
||||||
|
*
|
||||||
|
* @param url URL to treat.
|
||||||
|
* @return URL without scheme and prefix.
|
||||||
|
*/
|
||||||
|
removeCustomURLLinkScheme(url: string): string {
|
||||||
|
return url.replace(CoreConfigConstants.customurlscheme + '://link=', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the scheme and the "token=" prefix from a token custom URL.
|
||||||
|
*
|
||||||
|
* @param url URL to treat.
|
||||||
|
* @return URL without scheme and prefix.
|
||||||
|
*/
|
||||||
|
removeCustomURLTokenScheme(url: string): string {
|
||||||
|
return url.replace(CoreConfigConstants.customurlscheme + '://token=', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Treat error returned by handleCustomURL.
|
||||||
|
*
|
||||||
|
* @param error Error data.
|
||||||
|
*/
|
||||||
|
treatHandleCustomURLError(error: CoreCustomURLSchemesHandleError): void {
|
||||||
|
if (error.error == 'Duplicated') {
|
||||||
|
// Duplicated request
|
||||||
|
} else if (error.error && error.data && error.data.isSSOToken) {
|
||||||
|
// An error occurred, display the error and logout the user.
|
||||||
|
this.loginHelper.treatUserTokenError(error.data.siteUrl, error.error);
|
||||||
|
this.sitesProvider.logout();
|
||||||
|
} else {
|
||||||
|
this.domUtils.showErrorModalDefault(error.error, this.translate.instant('core.login.invalidsite'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error returned by handleCustomURL.
|
||||||
|
*/
|
||||||
|
export class CoreCustomURLSchemesHandleError {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param error The error message or object.
|
||||||
|
* @param data Data obtained from the URL (if any).
|
||||||
|
*/
|
||||||
|
constructor(public error: any, public data?: CoreCustomURLSchemesParams) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CoreCustomURLSchemes extends makeSingleton(CoreCustomURLSchemesProvider) {}
|
export class CoreCustomURLSchemes extends makeSingleton(CoreCustomURLSchemesProvider) {}
|
||||||
|
|
|
@ -104,6 +104,33 @@ export class CoreTextUtilsProvider {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add some text to an error message.
|
||||||
|
*
|
||||||
|
* @param error Error message or object.
|
||||||
|
* @param text Text to add.
|
||||||
|
* @return Modified error.
|
||||||
|
*/
|
||||||
|
addTextToError(error: string | CoreTextErrorObject, text: string): string | CoreTextErrorObject {
|
||||||
|
if (typeof error == 'string') {
|
||||||
|
return error + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (typeof error.message == 'string') {
|
||||||
|
error.message += text;
|
||||||
|
} else if (typeof error.error == 'string') {
|
||||||
|
error.error += text;
|
||||||
|
} else if (typeof error.content == 'string') {
|
||||||
|
error.content += text;
|
||||||
|
} else if (typeof error.body == 'string') {
|
||||||
|
error.body += text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an address as a string, return a URL to open the address in maps.
|
* Given an address as a string, return a URL to open the address in maps.
|
||||||
*
|
*
|
||||||
|
@ -414,28 +441,20 @@ export class CoreTextUtilsProvider {
|
||||||
* @param contextLevel The context level.
|
* @param contextLevel The context level.
|
||||||
* @param instanceId The instance ID related to the context.
|
* @param instanceId The instance ID related to the context.
|
||||||
* @param courseId Course ID the text belongs to. It can be used to improve performance with filters.
|
* @param courseId Course ID the text belongs to. It can be used to improve performance with filters.
|
||||||
|
* @deprecated since 3.8.3. Please use viewText instead.
|
||||||
*/
|
*/
|
||||||
expandText(title: string, text: string, component?: string, componentId?: string | number, files?: any[],
|
expandText(title: string, text: string, component?: string, componentId?: string | number, files?: any[],
|
||||||
filter?: boolean, contextLevel?: string, instanceId?: number, courseId?: number): void {
|
filter?: boolean, contextLevel?: string, instanceId?: number, courseId?: number): void {
|
||||||
if (text.length > 0) {
|
|
||||||
const params: any = {
|
|
||||||
title: title,
|
|
||||||
content: text,
|
|
||||||
component: component,
|
|
||||||
componentId: componentId,
|
|
||||||
files: files,
|
|
||||||
filter: filter,
|
|
||||||
contextLevel: contextLevel,
|
|
||||||
instanceId: instanceId,
|
|
||||||
courseId: courseId
|
|
||||||
};
|
|
||||||
|
|
||||||
// Open a modal with the contents.
|
return this.viewText(title, text, {
|
||||||
params.isModal = true;
|
component,
|
||||||
|
componentId,
|
||||||
const modal = this.modalCtrl.create('CoreViewerTextPage', params);
|
files,
|
||||||
modal.present();
|
filter,
|
||||||
}
|
contextLevel,
|
||||||
|
instanceId,
|
||||||
|
courseId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1106,6 +1125,50 @@ export class CoreTextUtilsProvider {
|
||||||
|
|
||||||
return _unserialize((data + ''), 0)[2];
|
return _unserialize((data + ''), 0)[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a text on a new page.
|
||||||
|
*
|
||||||
|
* @param title Title of the new state.
|
||||||
|
* @param text Content of the text to be expanded.
|
||||||
|
* @param component Component to link the embedded files to.
|
||||||
|
* @param componentId An ID to use in conjunction with the component.
|
||||||
|
* @param files List of files to display along with the text.
|
||||||
|
* @param filter Whether the text should be filtered.
|
||||||
|
* @param contextLevel The context level.
|
||||||
|
* @param instanceId The instance ID related to the context.
|
||||||
|
* @param courseId Course ID the text belongs to. It can be used to improve performance with filters.
|
||||||
|
*/
|
||||||
|
viewText(title: string, text: string, options?: CoreTextUtilsViewTextOptions): void {
|
||||||
|
if (text.length > 0) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
title: title,
|
||||||
|
content: text,
|
||||||
|
isModal: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(params, options);
|
||||||
|
|
||||||
|
const modal = this.modalCtrl.create('CoreViewerTextPage', params);
|
||||||
|
modal.present();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for viewText.
|
||||||
|
*/
|
||||||
|
export type CoreTextUtilsViewTextOptions = {
|
||||||
|
component?: string; // Component to link the embedded files to.
|
||||||
|
componentId?: string | number; // An ID to use in conjunction with the component.
|
||||||
|
files?: any[]; // List of files to display along with the text.
|
||||||
|
filter?: boolean; // Whether the text should be filtered.
|
||||||
|
contextLevel?: string; // The context level.
|
||||||
|
instanceId?: number; // The instance ID related to the context.
|
||||||
|
courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
||||||
|
displayCopyButton?: boolean; // Whether to display a button to copy the text.
|
||||||
|
};
|
||||||
|
|
||||||
export class CoreTextUtils extends makeSingleton(CoreTextUtilsProvider) {}
|
export class CoreTextUtils extends makeSingleton(CoreTextUtilsProvider) {}
|
||||||
|
|
|
@ -13,11 +13,12 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable, NgZone } from '@angular/core';
|
import { Injectable, NgZone } from '@angular/core';
|
||||||
import { Platform } from 'ionic-angular';
|
import { Platform, ModalController } from 'ionic-angular';
|
||||||
import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser';
|
import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser';
|
||||||
import { Clipboard } from '@ionic-native/clipboard';
|
import { Clipboard } from '@ionic-native/clipboard';
|
||||||
import { FileOpener } from '@ionic-native/file-opener';
|
import { FileOpener } from '@ionic-native/file-opener';
|
||||||
import { WebIntent } from '@ionic-native/web-intent';
|
import { WebIntent } from '@ionic-native/web-intent';
|
||||||
|
import { QRScanner } from '@ionic-native/qr-scanner';
|
||||||
import { CoreAppProvider } from '../app';
|
import { CoreAppProvider } from '../app';
|
||||||
import { CoreDomUtilsProvider } from './dom';
|
import { CoreDomUtilsProvider } from './dom';
|
||||||
import { CoreMimetypeUtilsProvider } from './mimetype';
|
import { CoreMimetypeUtilsProvider } from './mimetype';
|
||||||
|
@ -28,6 +29,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CoreLangProvider } from '../lang';
|
import { CoreLangProvider } from '../lang';
|
||||||
import { CoreWSProvider, CoreWSError } from '../ws';
|
import { CoreWSProvider, CoreWSError } from '../ws';
|
||||||
import { CoreFile } from '../file';
|
import { CoreFile } from '../file';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
import { makeSingleton } from '@singletons/core.singletons';
|
import { makeSingleton } from '@singletons/core.singletons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,12 +65,25 @@ export class CoreUtilsProvider {
|
||||||
protected logger;
|
protected logger;
|
||||||
protected iabInstance: InAppBrowserObject;
|
protected iabInstance: InAppBrowserObject;
|
||||||
protected uniqueIds: {[name: string]: number} = {};
|
protected uniqueIds: {[name: string]: number} = {};
|
||||||
|
protected qrScanData: {deferred: PromiseDefer, observable: Subscription};
|
||||||
|
|
||||||
constructor(private iab: InAppBrowser, private appProvider: CoreAppProvider, private clipboard: Clipboard,
|
constructor(protected iab: InAppBrowser,
|
||||||
private domUtils: CoreDomUtilsProvider, logger: CoreLoggerProvider, private translate: TranslateService,
|
protected appProvider: CoreAppProvider,
|
||||||
private platform: Platform, private langProvider: CoreLangProvider, private eventsProvider: CoreEventsProvider,
|
protected clipboard: Clipboard,
|
||||||
private fileOpener: FileOpener, private mimetypeUtils: CoreMimetypeUtilsProvider, private webIntent: WebIntent,
|
protected domUtils: CoreDomUtilsProvider,
|
||||||
private wsProvider: CoreWSProvider, private zone: NgZone, private textUtils: CoreTextUtilsProvider) {
|
logger: CoreLoggerProvider,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected platform: Platform,
|
||||||
|
protected langProvider: CoreLangProvider,
|
||||||
|
protected eventsProvider: CoreEventsProvider,
|
||||||
|
protected fileOpener: FileOpener,
|
||||||
|
protected mimetypeUtils: CoreMimetypeUtilsProvider,
|
||||||
|
protected webIntent: WebIntent,
|
||||||
|
protected wsProvider: CoreWSProvider,
|
||||||
|
protected zone: NgZone,
|
||||||
|
protected textUtils: CoreTextUtilsProvider,
|
||||||
|
protected modalCtrl: ModalController,
|
||||||
|
protected qrScanner: QRScanner) {
|
||||||
this.logger = logger.getInstance('CoreUtilsProvider');
|
this.logger = logger.getInstance('CoreUtilsProvider');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1420,6 +1435,117 @@ export class CoreUtilsProvider {
|
||||||
|
|
||||||
return debounced;
|
return debounced;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the app can scan QR codes.
|
||||||
|
*
|
||||||
|
* @return Whether the app can scan QR codes.
|
||||||
|
*/
|
||||||
|
canScanQR(): boolean {
|
||||||
|
return this.appProvider.isMobile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a modal to scan a QR code.
|
||||||
|
*
|
||||||
|
* @param title Title of the modal. Defaults to "QR reader".
|
||||||
|
* @return Promise resolved with the captured text or undefined if cancelled or error.
|
||||||
|
*/
|
||||||
|
scanQR(title?: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject): void => {
|
||||||
|
const modal = this.modalCtrl.create('CoreViewerQRScannerPage', {
|
||||||
|
title: title
|
||||||
|
}, { cssClass: 'core-modal-fullscreen'});
|
||||||
|
|
||||||
|
modal.present();
|
||||||
|
|
||||||
|
modal.onDidDismiss((data) => {
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start scanning for a QR code.
|
||||||
|
*
|
||||||
|
* @return Promise resolved with the QR string, rejected if error or cancelled.
|
||||||
|
*/
|
||||||
|
startScanQR(): Promise<string> {
|
||||||
|
if (!this.appProvider.isMobile()) {
|
||||||
|
return Promise.reject('QRScanner isn\'t available in desktop apps.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask the user for permission to use the camera.
|
||||||
|
// The scan method also does this, but since it returns an Observable we wouldn't be able to detect if the user denied.
|
||||||
|
return this.qrScanner.prepare().then((status) => {
|
||||||
|
|
||||||
|
if (!status.authorized) {
|
||||||
|
// No access to the camera, reject. In android this shouldn't happen, denying access passes through catch.
|
||||||
|
return Promise.reject('The user denied camera access.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.qrScanData && this.qrScanData.deferred) {
|
||||||
|
// Already scanning.
|
||||||
|
return this.qrScanData.deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start scanning.
|
||||||
|
this.qrScanData = {
|
||||||
|
deferred: this.promiseDefer(),
|
||||||
|
observable: this.qrScanner.scan().subscribe((text) => {
|
||||||
|
|
||||||
|
// Text received, stop scanning and return the text.
|
||||||
|
this.stopScanQR(text, false);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show the camera.
|
||||||
|
return this.qrScanner.show().then(() => {
|
||||||
|
document.body.classList.add('core-scanning-qr');
|
||||||
|
|
||||||
|
return this.qrScanData.deferred.promise;
|
||||||
|
}, (err) => {
|
||||||
|
this.stopScanQR(err, true);
|
||||||
|
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch((err) => {
|
||||||
|
err.message = err.message || err._message;
|
||||||
|
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop scanning for QR code. If no param is provided, the app will consider the user cancelled.
|
||||||
|
*
|
||||||
|
* @param data If success, the text of the QR code. If error, the error object or message. Undefined for cancelled.
|
||||||
|
* @param error True if the data belongs to an error, false otherwise.
|
||||||
|
*/
|
||||||
|
stopScanQR(data?: any, error?: boolean): void {
|
||||||
|
|
||||||
|
if (!this.qrScanData) {
|
||||||
|
// Not scanning.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide camera preview.
|
||||||
|
document.body.classList.remove('core-scanning-qr');
|
||||||
|
this.qrScanner.hide();
|
||||||
|
|
||||||
|
this.qrScanData.observable.unsubscribe(); // Stop scanning.
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
this.qrScanData.deferred.reject(data);
|
||||||
|
} else if (typeof data != 'undefined') {
|
||||||
|
this.qrScanData.deferred.resolve(data);
|
||||||
|
} else {
|
||||||
|
this.qrScanData.deferred.reject({coreCanceled: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.qrScanData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CoreUtils extends makeSingleton(CoreUtilsProvider) {}
|
export class CoreUtils extends makeSingleton(CoreUtilsProvider) {}
|
||||||
|
|
Loading…
Reference in New Issue