MOBILE-2995 qr: Add scan QR option in more menu
parent
30a2710114
commit
e14b3ed0f8
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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) {}
|
||||
|
|
Loading…
Reference in New Issue