MOBILE-2995 qr: Add scan QR option in more menu

main
Dani Palou 2019-10-11 09:18:56 +02:00
parent 30a2710114
commit e14b3ed0f8
9 changed files with 317 additions and 9 deletions

View File

@ -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;
}
}
}

View File

@ -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",

View File

@ -31,6 +31,10 @@
<h2>{{item.label}}</h2>
</a>
</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 }}">
<ion-icon name="globe" item-start aria-hidden="true"></ion-icon>
<h2>{{ 'core.mainmenu.website' | translate }}</h2>

View File

@ -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.
*/

View File

@ -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>

View File

@ -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 {}

View File

@ -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();
}
}

View File

@ -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",

View File

@ -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<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) {}