feature(qrcode): reboot qrcode reader

This commit is contained in:
Matthew Raymer
2025-04-21 10:13:12 +00:00
parent 2b7b07441e
commit ff605b3676
11 changed files with 1676 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
import {
BarcodeScanner,
BarcodeFormat,
StartScanOptions,
LensFacing,
} from "@capacitor-mlkit/barcode-scanning";
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
import { logger } from "@/utils/logger";
export class CapacitorQRScanner implements QRScannerService {
private scanListener: ScanListener | null = null;
private isScanning = false;
async checkPermissions(): Promise<boolean> {
try {
const { camera } = await BarcodeScanner.checkPermissions();
return camera === "granted";
} catch (error) {
logger.error("Error checking camera permissions:", error);
return false;
}
}
async requestPermissions(): Promise<boolean> {
try {
// First check if we already have permissions
if (await this.checkPermissions()) {
return true;
}
// Request permissions if we don't have them
const { camera } = await BarcodeScanner.requestPermissions();
return camera === "granted";
} catch (error) {
logger.error("Error requesting camera permissions:", error);
return false;
}
}
async isSupported(): Promise<boolean> {
try {
const { supported } = await BarcodeScanner.isSupported();
return supported;
} catch (error) {
logger.error("Error checking scanner support:", error);
return false;
}
}
async startScan(options?: QRScannerOptions): Promise<void> {
if (this.isScanning) {
return;
}
try {
// Ensure we have permissions before starting
logger.log('Checking camera permissions...');
if (!(await this.checkPermissions())) {
logger.log('Requesting camera permissions...');
const granted = await this.requestPermissions();
if (!granted) {
throw new Error("Camera permission denied");
}
}
// Check if scanning is supported
logger.log('Checking scanner support...');
if (!(await this.isSupported())) {
throw new Error("QR scanning not supported on this device");
}
logger.log('Starting MLKit scanner...');
this.isScanning = true;
const scanOptions: StartScanOptions = {
formats: [BarcodeFormat.QrCode],
lensFacing:
options?.camera === "front" ? LensFacing.Front : LensFacing.Back,
};
logger.log('Scanner options:', scanOptions);
const result = await BarcodeScanner.scan(scanOptions);
logger.log('Scan result:', result);
if (result.barcodes.length > 0) {
this.scanListener?.onScan(result.barcodes[0].rawValue);
}
} catch (error) {
logger.error("Error during QR scan:", error);
this.scanListener?.onError?.(error as Error);
} finally {
this.isScanning = false;
}
}
async stopScan(): Promise<void> {
if (!this.isScanning) {
return;
}
try {
await BarcodeScanner.stopScan();
this.isScanning = false;
} catch (error) {
logger.error("Error stopping QR scan:", error);
this.scanListener?.onError?.(error as Error);
}
}
addListener(listener: ScanListener): void {
this.scanListener = listener;
}
async cleanup(): Promise<void> {
await this.stopScan();
this.scanListener = null;
}
}

View File

@@ -0,0 +1,63 @@
import { Capacitor } from "@capacitor/core";
import { QRScannerService } from "./types";
import { CapacitorQRScanner } from "./CapacitorQRScanner";
import { WebDialogQRScanner } from "./WebDialogQRScanner";
import { logger } from "@/utils/logger";
/**
* Factory class for creating QR scanner instances based on platform
*/
export class QRScannerFactory {
private static instance: QRScannerService | null = null;
private static isNativePlatform(): boolean {
const capacitorNative = Capacitor.isNativePlatform();
const isMobile = __IS_MOBILE__;
const platform = Capacitor.getPlatform();
logger.log('Platform detection:', {
capacitorNative,
isMobile,
platform,
userAgent: navigator.userAgent
});
// Force native scanner on Android/iOS
if (platform === 'android' || platform === 'ios') {
return true;
}
return capacitorNative || isMobile;
}
/**
* Get a QR scanner instance appropriate for the current platform
*/
static getInstance(): QRScannerService {
if (!this.instance) {
const isNative = this.isNativePlatform();
logger.log(`Creating QR scanner for platform: ${isNative ? 'native' : 'web'}`);
if (isNative) {
logger.log('Using native MLKit scanner');
this.instance = new CapacitorQRScanner();
} else if (__USE_QR_READER__) {
logger.log('Using web QR scanner');
this.instance = new WebDialogQRScanner();
} else {
throw new Error("No QR scanner implementation available for this platform");
}
}
return this.instance!; // We know it's not null here
}
/**
* Clean up the current scanner instance
*/
static async cleanup(): Promise<void> {
if (this.instance) {
await this.instance.cleanup();
this.instance = null;
}
}
}

View File

@@ -0,0 +1,108 @@
import { createApp, App } from "vue";
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
import QRScannerDialog from "@/components/QRScanner/QRScannerDialog.vue";
import { logger } from "@/utils/logger";
export class WebDialogQRScanner implements QRScannerService {
private dialogInstance: App | null = null;
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null;
private scanListener: ScanListener | null = null;
private isScanning = false;
constructor(private options?: QRScannerOptions) {}
async checkPermissions(): Promise<boolean> {
try {
const permissions = await navigator.permissions.query({
name: "camera" as PermissionName,
});
return permissions.state === "granted";
} catch (error) {
logger.error("Error checking camera permissions:", error);
return false;
}
}
async requestPermissions(): Promise<boolean> {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks().forEach((track) => track.stop());
return true;
} catch (error) {
logger.error("Error requesting camera permissions:", error);
return false;
}
}
async isSupported(): Promise<boolean> {
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
}
async startScan(): Promise<void> {
if (this.isScanning) {
return;
}
try {
this.isScanning = true;
// Create and mount dialog component
const container = document.createElement("div");
document.body.appendChild(container);
this.dialogInstance = createApp(QRScannerDialog, {
onScan: (result: string) => {
if (this.scanListener) {
this.scanListener.onScan(result);
}
},
onError: (error: Error) => {
if (this.scanListener?.onError) {
this.scanListener.onError(error);
}
},
options: this.options,
});
this.dialogComponent = this.dialogInstance.mount(container).$refs
.dialog as InstanceType<typeof QRScannerDialog>;
} catch (error) {
this.isScanning = false;
if (this.scanListener?.onError) {
this.scanListener.onError(
error instanceof Error ? error : new Error(String(error)),
);
}
logger.error("Error starting scan:", error);
}
}
async stopScan(): Promise<void> {
if (!this.isScanning) {
return;
}
try {
if (this.dialogComponent) {
await this.dialogComponent.close();
}
if (this.dialogInstance) {
this.dialogInstance.unmount();
}
this.isScanning = false;
} catch (error) {
logger.error("Error stopping scan:", error);
}
}
addListener(listener: ScanListener): void {
this.scanListener = listener;
}
async cleanup(): Promise<void> {
await this.stopScan();
this.dialogComponent = null;
this.dialogInstance = null;
this.scanListener = null;
}
}

View File

@@ -0,0 +1,49 @@
// QR Scanner Service Types
/**
* Listener interface for QR code scan events
*/
export interface ScanListener {
/** Called when a QR code is successfully scanned */
onScan: (result: string) => void;
/** Called when an error occurs during scanning */
onError?: (error: Error) => void;
}
/**
* Options for configuring the QR scanner
*/
export interface QRScannerOptions {
/** Camera to use ('front' or 'back') */
camera?: "front" | "back";
/** Whether to show a preview of the camera feed */
showPreview?: boolean;
/** Whether to play a sound on successful scan */
playSound?: boolean;
}
/**
* Interface for QR scanner service implementations
*/
export interface QRScannerService {
/** Check if camera permissions are granted */
checkPermissions(): Promise<boolean>;
/** Request camera permissions from the user */
requestPermissions(): Promise<boolean>;
/** Check if QR scanning is supported on this device */
isSupported(): Promise<boolean>;
/** Start scanning for QR codes */
startScan(options?: QRScannerOptions): Promise<void>;
/** Stop scanning for QR codes */
stopScan(): Promise<void>;
/** Add a listener for scan events */
addListener(listener: ScanListener): void;
/** Clean up scanner resources */
cleanup(): Promise<void>;
}