From 30a2710114fc38d4b8ebcfc6ac2a94f58c5b6599 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Thu, 10 Oct 2019 12:29:26 +0200 Subject: [PATCH 1/9] MOBILE-2995 qr: Add QR plugin in the app --- config.xml | 1 + package-lock.json | 31 +++++ package.json | 7 +- src/core/emulator/emulator.module.ts | 11 ++ src/core/emulator/providers/qr-scanner.ts | 160 ++++++++++++++++++++++ 5 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 src/core/emulator/providers/qr-scanner.ts diff --git a/config.xml b/config.xml index 1282a7a65..cd9f0ab61 100644 --- a/config.xml +++ b/config.xml @@ -170,6 +170,7 @@ + diff --git a/package-lock.json b/package-lock.json index 956c7923b..074dd3600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -182,6 +182,11 @@ "resolved": "https://registry.npmjs.org/@ionic-native/push/-/push-4.20.0.tgz", "integrity": "sha512-IgzaZd8KSPLwyLX1emRijlQ0Vfa3RlPPBx370lVH32c8zG3DFH1xfQQbb39KF3qmX5b6so0pGGA2holSUwVm2w==" }, + "@ionic-native/qr-scanner": { + "version": "4.20.0", + "resolved": "https://registry.npmjs.org/@ionic-native/qr-scanner/-/qr-scanner-4.20.0.tgz", + "integrity": "sha512-eLeJQq49/x5bdCVLotuMHZZ3YGEpSzuEnuX2vno2ugdGSygBm+wxIVSa9Nuz8HozYwC6oyii+zH/pg4SZ+4V9Q==" + }, "@ionic-native/screen-orientation": { "version": "4.20.0", "resolved": "https://registry.npmjs.org/@ionic-native/screen-orientation/-/screen-orientation-4.20.0.tgz", @@ -2705,6 +2710,14 @@ "resolved": "https://registry.npmjs.org/cordova-plugin-network-information/-/cordova-plugin-network-information-2.0.2.tgz", "integrity": "sha512-NwO3qDBNL/vJxUxBTPNOA1HvkDf9eTeGH8JSZiwy1jq2W2mJKQEDBwqWkaEQS19Yd/MQTiw0cykxg5D7u4J6cQ==" }, + "cordova-plugin-qrscanner": { + "version": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#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": { "version": "3.0.2", "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", "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, + "qrcode-reader": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/qrcode-reader/-/qrcode-reader-1.0.4.tgz", + "integrity": "sha512-rRjALGNh9zVqvweg1j5OKIQKNsw3bLC+7qwlnead5K/9cb1cEIAGkwikt/09U0K+2IDWGD9CC6SP7tHAjUeqvQ==" + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -11127,6 +11145,11 @@ } } }, + "sdp": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/sdp/-/sdp-1.5.4.tgz", + "integrity": "sha1-jgOPbdsUvXZa4fS1IW4SCUUR4NA=" + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -13864,6 +13887,14 @@ "source-map": "~0.6.1" } }, + "webrtc-adapter": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-3.4.3.tgz", + "integrity": "sha1-tjYGLu6abvFYrNDYUBtnhDS1bxY=", + "requires": { + "sdp": "^1.5.0" + } + }, "websocket-driver": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", diff --git a/package.json b/package.json index 941127fae..d1b669d91 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@ionic-native/media-capture": "4.20.0", "@ionic-native/network": "4.20.0", "@ionic-native/push": "4.20.0", + "@ionic-native/qr-scanner": "4.20.0", "@ionic-native/screen-orientation": "4.20.0", "@ionic-native/splash-screen": "4.20.0", "@ionic-native/sqlite": "4.20.0", @@ -98,6 +99,7 @@ "cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle", "cordova-plugin-media-capture": "3.0.3", "cordova-plugin-network-information": "2.0.2", + "cordova-plugin-qrscanner": "git+https://github.com/moodlemobile/cordova-plugin-qrscanner.git#dist", "cordova-plugin-screen-orientation": "3.0.2", "cordova-plugin-splashscreen": "5.0.3", "cordova-plugin-statusbar": "2.4.3", @@ -194,7 +196,8 @@ "cordova-plugin-advanced-http": { "OKHTTP_VERSION": "3.10.0" }, - "cordova-plugin-wkwebview-cookies": {} + "cordova-plugin-wkwebview-cookies": {}, + "cordova-plugin-qrscanner": {} } }, "main": "desktop/electron.js", @@ -254,4 +257,4 @@ "deleteAppDataOnUninstall": true } } -} +} \ No newline at end of file diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts index a24a2b08e..5f9886c9a 100644 --- a/src/core/emulator/emulator.module.ts +++ b/src/core/emulator/emulator.module.ts @@ -31,6 +31,7 @@ import { LocalNotifications } from '@ionic-native/local-notifications'; import { MediaCapture } from '@ionic-native/media-capture'; import { Network } from '@ionic-native/network'; import { Push } from '@ionic-native/push'; +import { QRScanner } from '@ionic-native/qr-scanner'; import { SplashScreen } from '@ionic-native/splash-screen'; import { StatusBar } from '@ionic-native/status-bar'; import { SQLite } from '@ionic-native/sqlite'; @@ -51,12 +52,14 @@ import { LocalNotificationsMock } from './providers/local-notifications'; import { MediaCaptureMock } from './providers/media-capture'; import { NetworkMock } from './providers/network'; import { PushMock } from './providers/push'; +import { QRScannerMock } from './providers/qr-scanner'; import { ZipMock } from './providers/zip'; import { CoreEmulatorHelperProvider } from './providers/helper'; import { CoreEmulatorCaptureHelperProvider } from './providers/capture-helper'; import { CoreAppProvider } from '@providers/app'; import { CoreFileProvider } from '@providers/file'; +import { CoreLoggerProvider } from '@providers/logger'; import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; import { CoreTextUtilsProvider } from '@providers/utils/text'; import { CoreUrlUtilsProvider } from '@providers/utils/url'; @@ -80,6 +83,7 @@ export const IONIC_NATIVE_PROVIDERS = [ MediaCapture, Network, Push, + QRScanner, SplashScreen, StatusBar, SQLite, @@ -206,6 +210,13 @@ export const IONIC_NATIVE_PROVIDERS = [ 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, StatusBar, SQLite, diff --git a/src/core/emulator/providers/qr-scanner.ts b/src/core/emulator/providers/qr-scanner.ts new file mode 100644 index 000000000..668a74f5c --- /dev/null +++ b/src/core/emulator/providers/qr-scanner.ts @@ -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 { + 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 { + 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 and DOM + * elements to transparent, allowing the webview to re-render with the transparent background. + * + * @return Promise. + */ + show(): Promise { + 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 { + 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 { + return Promise.reject('QRScanner isn\'t available in desktop apps.'); + } + + /** + * Destroy the scanner instance. + * + * @return Promise. + */ + destroy(): Promise { + return Promise.reject('QRScanner isn\'t available in desktop apps.'); + } + + /** + * Disable the device's light. + * + * @return Promise. + */ + disableLight(): Promise { + return Promise.reject('QRScanner isn\'t available in desktop apps.'); + } + + /** + * Use front camera + * + * @return Promise. + */ + useFrontCamera(): Promise { + return Promise.reject('QRScanner isn\'t available in desktop apps.'); + } + + /** + * Use back camera + * + * @return Promise. + */ + useBackCamera(): Promise { + 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 { + 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 { + return Promise.reject('QRScanner isn\'t available in desktop apps.'); + } + + /** + * Resumse the video preview and resumes scanning. + * + * @return Promise. + */ + resumePreview(): Promise { + return Promise.reject('QRScanner isn\'t available in desktop apps.'); + } + + /** + * Returns permission status + * + * @return Promise. + */ + getStatus(): Promise { + 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.'); + } +} From e14b3ed0f8628c71aa5014d21b7832fbf02366e9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 11 Oct 2019 09:18:56 +0200 Subject: [PATCH 2/9] MOBILE-2995 qr: Add scan QR option in more menu --- src/app/app.scss | 14 ++ src/assets/lang/en.json | 2 + src/core/mainmenu/pages/more/more.html | 4 + src/core/mainmenu/pages/more/more.ts | 43 +++++- .../viewer/pages/qr-scanner/qr-scanner.html | 13 ++ .../pages/qr-scanner/qr-scanner.module.ts | 31 ++++ .../viewer/pages/qr-scanner/qr-scanner.ts | 79 ++++++++++ src/lang/en.json | 2 + src/providers/utils/utils.ts | 138 +++++++++++++++++- 9 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 src/core/viewer/pages/qr-scanner/qr-scanner.html create mode 100644 src/core/viewer/pages/qr-scanner/qr-scanner.module.ts create mode 100644 src/core/viewer/pages/qr-scanner/qr-scanner.ts diff --git a/src/app/app.scss b/src/app/app.scss index b384030c3..064b7bfdb 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -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; + } + } +} diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 7f19738e8..193426e84 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1868,6 +1868,7 @@ "core.previous": "Previous", "core.proceed": "Proceed", "core.pulltorefresh": "Pull to refresh", + "core.qrscanner": "QR scanner", "core.question.answer": "Answer", "core.question.answersaved": "Answer saved", "core.question.cannotdeterminestatus": "Cannot determine status", @@ -1910,6 +1911,7 @@ "core.retry": "Retry", "core.save": "Save", "core.savechanges": "Save changes", + "core.scanqr": "Scan QR code", "core.search": "Search", "core.searching": "Searching", "core.searchresults": "Search results", diff --git a/src/core/mainmenu/pages/more/more.html b/src/core/mainmenu/pages/more/more.html index d2d3908d1..4b2949e7a 100644 --- a/src/core/mainmenu/pages/more/more.html +++ b/src/core/mainmenu/pages/more/more.html @@ -31,6 +31,10 @@

{{item.label}}

+ + +

{{ 'core.scanqr' | translate }}

+

{{ 'core.mainmenu.website' | translate }}

diff --git a/src/core/mainmenu/pages/more/more.ts b/src/core/mainmenu/pages/more/more.ts index 864402eef..ebcab3684 100644 --- a/src/core/mainmenu/pages/more/more.ts +++ b/src/core/mainmenu/pages/more/more.ts @@ -16,9 +16,13 @@ import { Component, OnDestroy } from '@angular/core'; import { IonicPage, NavController } from 'ionic-angular'; import { CoreEventsProvider } from '@providers/events'; import { CoreSitesProvider } from '@providers/sites'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreUtilsProvider } from '@providers/utils/utils'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate'; import { CoreMainMenuProvider, CoreMainMenuCustomItem } from '../../providers/mainmenu'; 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. @@ -35,6 +39,7 @@ export class CoreMainMenuMorePage implements OnDestroy { siteInfo: any; siteName: string; logoutLabel: string; + showScanQR: boolean; showWeb: boolean; showHelp: boolean; docsUrl: string; @@ -45,14 +50,22 @@ export class CoreMainMenuMorePage implements OnDestroy { protected langObserver; protected updateSiteObserver; - constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider, - private navCtrl: NavController, private mainMenuProvider: CoreMainMenuProvider, - eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider) { + constructor(private menuDelegate: CoreMainMenuDelegate, + private sitesProvider: CoreSitesProvider, + private navCtrl: NavController, + private mainMenuProvider: CoreMainMenuProvider, + eventsProvider: CoreEventsProvider, + private loginHelper: CoreLoginHelperProvider, + private utils: CoreUtilsProvider, + private linkHelper: CoreContentLinksHelperProvider, + private textUtils: CoreTextUtilsProvider, + private translate: TranslateService) { this.langObserver = eventsProvider.on(CoreEventsProvider.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this)); this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.loadSiteInfo.bind(this), sitesProvider.getCurrentSiteId()); this.loadSiteInfo(); + this.showScanQR = this.utils.canScanQR(); } /** @@ -155,6 +168,30 @@ export class CoreMainMenuMorePage implements OnDestroy { this.navCtrl.push('CoreSitePreferencesPage'); } + /** + * Scan and treat a QR code. + */ + scanQR(): void { + // Scan for a QR code. + this.utils.scanQR().then((text) => { + if (text) { + // Check if it's a URL. We basically check it has a protocol and doesn't include any space. + if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) { + // 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.expandText(this.translate.instant('core.qrscanner'), text); + } + } + }); + } + /** * Logout the user. */ diff --git a/src/core/viewer/pages/qr-scanner/qr-scanner.html b/src/core/viewer/pages/qr-scanner/qr-scanner.html new file mode 100644 index 000000000..dac2e266c --- /dev/null +++ b/src/core/viewer/pages/qr-scanner/qr-scanner.html @@ -0,0 +1,13 @@ + + + {{ title }} + + + + + + + + diff --git a/src/core/viewer/pages/qr-scanner/qr-scanner.module.ts b/src/core/viewer/pages/qr-scanner/qr-scanner.module.ts new file mode 100644 index 000000000..db7f83fa7 --- /dev/null +++ b/src/core/viewer/pages/qr-scanner/qr-scanner.module.ts @@ -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 {} diff --git a/src/core/viewer/pages/qr-scanner/qr-scanner.ts b/src/core/viewer/pages/qr-scanner/qr-scanner.ts new file mode 100644 index 000000000..49a2060e7 --- /dev/null +++ b/src/core/viewer/pages/qr-scanner/qr-scanner.ts @@ -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(); + } +} diff --git a/src/lang/en.json b/src/lang/en.json index 7764553bb..cc4d99e42 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -209,6 +209,7 @@ "previous": "Previous", "proceed": "Proceed", "pulltorefresh": "Pull to refresh", + "qrscanner": "QR scanner", "quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.", "redirectingtosite": "You will be redirected to the site.", "refresh": "Refresh", @@ -223,6 +224,7 @@ "retry": "Retry", "save": "Save", "savechanges": "Save changes", + "scanqr": "Scan QR code", "search": "Search", "searching": "Searching", "searchresults": "Search results", diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index b3781a706..49d562d18 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -13,11 +13,12 @@ // limitations under the License. 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 { Clipboard } from '@ionic-native/clipboard'; import { FileOpener } from '@ionic-native/file-opener'; import { WebIntent } from '@ionic-native/web-intent'; +import { QRScanner } from '@ionic-native/qr-scanner'; import { CoreAppProvider } from '../app'; import { CoreDomUtilsProvider } from './dom'; import { CoreMimetypeUtilsProvider } from './mimetype'; @@ -28,6 +29,7 @@ import { TranslateService } from '@ngx-translate/core'; import { CoreLangProvider } from '../lang'; import { CoreWSProvider, CoreWSError } from '../ws'; import { CoreFile } from '../file'; +import { Subscription } from 'rxjs'; import { makeSingleton } from '@singletons/core.singletons'; /** @@ -63,12 +65,25 @@ export class CoreUtilsProvider { protected logger; protected iabInstance: InAppBrowserObject; protected uniqueIds: {[name: string]: number} = {}; + protected qrScanData: {deferred: PromiseDefer, observable: Subscription}; - constructor(private iab: InAppBrowser, private appProvider: CoreAppProvider, private clipboard: Clipboard, - private domUtils: CoreDomUtilsProvider, logger: CoreLoggerProvider, private translate: TranslateService, - private platform: Platform, private langProvider: CoreLangProvider, private eventsProvider: CoreEventsProvider, - private fileOpener: FileOpener, private mimetypeUtils: CoreMimetypeUtilsProvider, private webIntent: WebIntent, - private wsProvider: CoreWSProvider, private zone: NgZone, private textUtils: CoreTextUtilsProvider) { + constructor(protected iab: InAppBrowser, + protected appProvider: CoreAppProvider, + protected clipboard: Clipboard, + protected domUtils: CoreDomUtilsProvider, + 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'); } @@ -1420,6 +1435,117 @@ export class CoreUtilsProvider { 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 { + 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 { + 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) {} From 394dd6e24d8fe1bfddc00657ebc1be17587671fa Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 11 Oct 2019 12:44:33 +0200 Subject: [PATCH 3/9] MOBILE-2995 qr: Add scan QR button in login --- src/core/login/pages/site/site.html | 5 +++++ src/core/login/pages/site/site.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/core/login/pages/site/site.html b/src/core/login/pages/site/site.html index 9a4fe01bc..637c8c90e 100644 --- a/src/core/login/pages/site/site.html +++ b/src/core/login/pages/site/site.html @@ -47,6 +47,11 @@ {{site.name}} + +
+ + {{ 'core.scanqr' | translate }} + diff --git a/src/core/login/pages/site/site.ts b/src/core/login/pages/site/site.ts index 180e0ed47..4546cf012 100644 --- a/src/core/login/pages/site/site.ts +++ b/src/core/login/pages/site/site.ts @@ -58,6 +58,7 @@ export class CoreLoginSitePage { loadingSites = false; onlyWrittenSite = false; searchFnc: Function; + showScanQR: boolean; constructor(navParams: NavParams, protected navCtrl: NavController, @@ -74,6 +75,7 @@ export class CoreLoginSitePage { protected utils: CoreUtilsProvider) { this.showKeyboard = !!navParams.get('showKeyboard'); + this.showScanQR = this.utils.canScanQR(); let url = ''; @@ -356,4 +358,17 @@ export class CoreLoginSitePage { }; } + /** + * Scan a QR code and put its text in the URL input. + */ + scanQR(): void { + // Scan for a QR code. + this.utils.scanQR().then((text) => { + if (text) { + this.siteForm.controls.siteUrl.setValue(text); + + this.connect(new Event('click'), text); + } + }); + } } From c3f39abafa40514dd34fd0c0c29bf2300cbbf627 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 11 Oct 2019 16:02:28 +0200 Subject: [PATCH 4/9] MOBILE-2995 qr: Add QR button in RTE --- .../core-editor-rich-text-editor.html | 5 +++++ .../rich-text-editor/rich-text-editor.ts | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/core/editor/components/rich-text-editor/core-editor-rich-text-editor.html b/src/core/editor/components/rich-text-editor/core-editor-rich-text-editor.html index 20faba16e..4423c4b7a 100644 --- a/src/core/editor/components/rich-text-editor/core-editor-rich-text-editor.html +++ b/src/core/editor/components/rich-text-editor/core-editor-rich-text-editor.html @@ -71,6 +71,11 @@ + + + + diff --git a/src/core/viewer/pages/text/text.scss b/src/core/viewer/pages/text/text.scss new file mode 100644 index 000000000..4377e35e3 --- /dev/null +++ b/src/core/viewer/pages/text/text.scss @@ -0,0 +1,5 @@ +ion-app.app-root page-core-viewer-text { + ion-footer { + padding: 6px; + } +} diff --git a/src/core/viewer/pages/text/text.ts b/src/core/viewer/pages/text/text.ts index 6c8c1b6f8..935978391 100644 --- a/src/core/viewer/pages/text/text.ts +++ b/src/core/viewer/pages/text/text.ts @@ -15,6 +15,7 @@ import { Component } from '@angular/core'; import { IonicPage, ViewController, NavParams } from 'ionic-angular'; 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. @@ -34,6 +35,7 @@ export class CoreViewerTextPage { 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 contents. constructor(private viewCtrl: ViewController, params: NavParams, textUtils: CoreTextUtilsProvider) { this.title = params.get('title'); @@ -45,6 +47,7 @@ export class CoreViewerTextPage { this.contextLevel = params.get('contextLevel'); this.instanceId = params.get('instanceId'); this.courseId = params.get('courseId'); + this.displayCopyButton = !!params.get('displayCopyButton'); } /** @@ -53,4 +56,11 @@ export class CoreViewerTextPage { closeModal(): void { this.viewCtrl.dismiss(); } + + /** + * Copy the text to clipboard. + */ + copyText(): void { + CoreUtils.instance.copyToClipboard(this.content); + } } diff --git a/src/directives/format-text.ts b/src/directives/format-text.ts index fdec21043..f5c9f8f4b 100644 --- a/src/directives/format-text.ts +++ b/src/directives/format-text.ts @@ -307,8 +307,14 @@ export class CoreFormatTextDirective implements OnChanges { // Open a new state with the contents. 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.component, this.componentId, undefined, filter, this.contextLevel, this.contextInstanceId, this.courseId); + this.textUtils.viewText(this.fullTitle || this.translate.instant('core.description'), this.text, { + component: this.component, + componentId: this.componentId, + filter: filter, + contextLevel: this.contextLevel, + instanceId: this.contextInstanceId, + courseId: this.courseId, + }); } } diff --git a/src/lang/en.json b/src/lang/en.json index cc4d99e42..8d279f23c 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -51,6 +51,7 @@ "contenteditingsynced": "The content you are editing has been synced.", "continue": "Continue", "copiedtoclipboard": "Text copied to clipboard", + "copytoclipboard": "Copy to clipboard", "course": "Course", "coursedetails": "Course details", "coursenogroups": "You are not a member of any group of this course.", diff --git a/src/providers/utils/text.ts b/src/providers/utils/text.ts index 3cac7f846..5ba186d9b 100644 --- a/src/providers/utils/text.ts +++ b/src/providers/utils/text.ts @@ -441,28 +441,20 @@ export class CoreTextUtilsProvider { * @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. + * @deprecated since 3.8.3. Please use viewText instead. */ expandText(title: string, text: string, component?: string, componentId?: string | number, files?: any[], 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. - params.isModal = true; - - const modal = this.modalCtrl.create('CoreViewerTextPage', params); - modal.present(); - } + return this.viewText(title, text, { + component, + componentId, + files, + filter, + contextLevel, + instanceId, + courseId, + }); } /** @@ -1133,6 +1125,50 @@ export class CoreTextUtilsProvider { 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) {}