MOBILE-2995 qr: Add scan QR option in more menu
This commit is contained in:
		
							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. | ||||
|      */ | ||||
|  | ||||
							
								
								
									
										13
									
								
								src/core/viewer/pages/qr-scanner/qr-scanner.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/core/viewer/pages/qr-scanner/qr-scanner.html
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										31
									
								
								src/core/viewer/pages/qr-scanner/qr-scanner.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/core/viewer/pages/qr-scanner/qr-scanner.module.ts
									
									
									
									
									
										Normal 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 {} | ||||
							
								
								
									
										79
									
								
								src/core/viewer/pages/qr-scanner/qr-scanner.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/core/viewer/pages/qr-scanner/qr-scanner.ts
									
									
									
									
									
										Normal 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(); | ||||
|     } | ||||
| } | ||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user