WIP: Enhance QR scanner initialization and error handling
- Add visual processing overlay with status feedback - Implement robust worker initialization with retry mechanism - Add detailed error tracking and logging - Improve image processing with proper cleanup - Handle worker lifecycle and state management - Add proper module context simulation for jsQR initialization Still investigating jsQR initialization failure in worker context.
This commit is contained in:
@@ -2,6 +2,15 @@
|
|||||||
<QuickNav selected="Profile" />
|
<QuickNav selected="Profile" />
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<!-- Processing Overlay -->
|
||||||
|
<div v-if="isProcessing" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg p-6 text-center">
|
||||||
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||||
|
<p class="text-lg font-semibold">{{ processingStatus }}</p>
|
||||||
|
<p class="text-sm text-gray-600 mt-2">{{ processingDetails }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
@@ -202,6 +211,20 @@ interface WorkerErrorMessage {
|
|||||||
|
|
||||||
type WorkerMessage = WorkerSuccessMessage | WorkerErrorMessage;
|
type WorkerMessage = WorkerSuccessMessage | WorkerErrorMessage;
|
||||||
|
|
||||||
|
interface WorkerResult {
|
||||||
|
success: boolean;
|
||||||
|
code?: {
|
||||||
|
data: string;
|
||||||
|
location: {
|
||||||
|
topLeft: { x: number; y: number };
|
||||||
|
topRight: { x: number; y: number };
|
||||||
|
bottomLeft: { x: number; y: number };
|
||||||
|
bottomRight: { x: number; y: number };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null,
|
QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null,
|
||||||
@@ -238,34 +261,23 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
private isCapturingPhoto = false;
|
private isCapturingPhoto = false;
|
||||||
private appStateListener?: { remove: () => Promise<void> };
|
private appStateListener?: { remove: () => Promise<void> };
|
||||||
|
|
||||||
|
private isProcessing = false;
|
||||||
|
private processingStatus = '';
|
||||||
|
private processingDetails = '';
|
||||||
|
|
||||||
|
private worker: Worker | null = null;
|
||||||
|
private workerInitialized = false;
|
||||||
|
private initializationAttempts = 0;
|
||||||
|
private readonly MAX_INITIALIZATION_ATTEMPTS = 3;
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
logger.log('ContactQRScanShow component created');
|
logger.log('ContactQRScanShow component created');
|
||||||
try {
|
try {
|
||||||
// Remove any existing listeners first
|
// Remove any existing listeners first
|
||||||
await App.removeAllListeners();
|
await this.cleanupAppListeners();
|
||||||
|
|
||||||
// Add app state listeners
|
// Add app state listeners
|
||||||
const stateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
|
await this.setupAppLifecycleListeners();
|
||||||
logger.log('App state changed:', state);
|
|
||||||
if (!state.isActive && this.cameraActive) {
|
|
||||||
this.cleanupCamera();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.appStateListener = stateListener;
|
|
||||||
|
|
||||||
await App.addListener('pause', () => {
|
|
||||||
logger.log('App paused');
|
|
||||||
if (this.cameraActive) {
|
|
||||||
this.cleanupCamera();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await App.addListener('resume', () => {
|
|
||||||
logger.log('App resumed');
|
|
||||||
if (this.cameraActive) {
|
|
||||||
this.initializeCamera();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
await this.loadInitialData();
|
await this.loadInitialData();
|
||||||
@@ -277,6 +289,50 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async cleanupAppListeners(): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (this.appStateListener) {
|
||||||
|
await this.appStateListener.remove();
|
||||||
|
this.appStateListener = undefined;
|
||||||
|
}
|
||||||
|
await App.removeAllListeners();
|
||||||
|
logger.log('App listeners cleaned up successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error cleaning up app listeners:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setupAppLifecycleListeners(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Add app state change listener
|
||||||
|
this.appStateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
|
||||||
|
logger.log('App state changed:', state);
|
||||||
|
if (!state.isActive) {
|
||||||
|
this.cleanupCamera();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add pause listener
|
||||||
|
await App.addListener('pause', () => {
|
||||||
|
logger.log('App paused');
|
||||||
|
this.cleanupCamera();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add resume listener
|
||||||
|
await App.addListener('resume', () => {
|
||||||
|
logger.log('App resumed');
|
||||||
|
// Don't automatically reinitialize camera on resume
|
||||||
|
// Let user explicitly request camera access again
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log('App lifecycle listeners setup complete');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error setting up app lifecycle listeners:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async loadInitialData() {
|
private async loadInitialData() {
|
||||||
try {
|
try {
|
||||||
// Load settings from DB
|
// Load settings from DB
|
||||||
@@ -292,22 +348,34 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanupCamera() {
|
private async cleanupCamera() {
|
||||||
try {
|
try {
|
||||||
this.cameraActive = false;
|
if (this.cameraActive) {
|
||||||
this.isCapturingPhoto = false;
|
this.cameraActive = false;
|
||||||
this.addCameraState('cleanup');
|
this.isCapturingPhoto = false;
|
||||||
logger.log('Camera cleaned up successfully');
|
this.addCameraState('cleanup');
|
||||||
|
logger.log('Camera cleaned up successfully');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error during camera cleanup:', error);
|
logger.error('Error during camera cleanup:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addCameraState(state: CameraState | QRProcessingState, details?: Record<string, unknown>) {
|
private addCameraState(state: CameraState | QRProcessingState, details?: Record<string, unknown>) {
|
||||||
|
// Prevent duplicate state transitions
|
||||||
|
if (this.lastCameraState === state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const entry: CameraStateHistoryEntry = {
|
const entry: CameraStateHistoryEntry = {
|
||||||
state,
|
state,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
details
|
details: {
|
||||||
|
...details,
|
||||||
|
cameraActive: this.cameraActive,
|
||||||
|
isCapturingPhoto: this.isCapturingPhoto,
|
||||||
|
isProcessing: this.isProcessing
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.cameraStateHistory.push(entry);
|
this.cameraStateHistory.push(entry);
|
||||||
@@ -316,17 +384,11 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.cameraStateHistory.shift();
|
this.cameraStateHistory.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced logging with better details
|
this.logWithDetails('Camera state transition', {
|
||||||
logger.log('Camera state transition:', {
|
state: entry.state,
|
||||||
state,
|
timestamp: entry.timestamp,
|
||||||
details: {
|
details: entry.details
|
||||||
...details,
|
|
||||||
cameraActive: this.cameraActive,
|
|
||||||
isCapturingPhoto: this.isCapturingPhoto,
|
|
||||||
historyLength: this.cameraStateHistory.length
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.lastCameraState = state;
|
this.lastCameraState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +425,11 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
]).catch(error => {
|
]).catch(error => {
|
||||||
logger.error('Error during component cleanup:', error);
|
logger.error('Error during component cleanup:', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.worker) {
|
||||||
|
this.worker.terminate();
|
||||||
|
this.worker = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleQRCodeResult(data: string): Promise<void> {
|
private async handleQRCodeResult(data: string): Promise<void> {
|
||||||
@@ -456,114 +523,376 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async processImageForQRCode(imageDataUrl: string): Promise<void> {
|
private async initializeWorker(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.log('Starting QR code processing');
|
if (this.initializationAttempts >= this.MAX_INITIALIZATION_ATTEMPTS) {
|
||||||
|
throw new Error('Maximum worker initialization attempts reached');
|
||||||
|
}
|
||||||
|
|
||||||
// Create worker for image processing
|
this.initializationAttempts++;
|
||||||
const worker = new Worker(URL.createObjectURL(new Blob([`
|
this.setProcessingStatus('Initializing', `Setting up QR scanner (attempt ${this.initializationAttempts})...`);
|
||||||
self.onmessage = async function(e) {
|
logger.log('Starting worker initialization', { attempt: this.initializationAttempts });
|
||||||
|
|
||||||
|
// Import and wait for jsQR to be fully loaded
|
||||||
|
logger.log('Loading jsQR module...');
|
||||||
|
const jsQRModule = await import('jsqr');
|
||||||
|
if (!jsQRModule?.default) {
|
||||||
|
throw new Error('Failed to load jsQR module');
|
||||||
|
}
|
||||||
|
logger.log('jsQR module loaded successfully');
|
||||||
|
|
||||||
|
// Create worker with inline code and bundled jsQR
|
||||||
|
const workerCode = `
|
||||||
|
let jsQRInitialized = false;
|
||||||
|
let initializationError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the raw jsQR function code
|
||||||
|
const jsQRCode = ${jsQRModule.default.toString()};
|
||||||
|
|
||||||
|
// Create a proper module-like environment
|
||||||
|
const context = {
|
||||||
|
module: { exports: {} },
|
||||||
|
exports: {},
|
||||||
|
self: self,
|
||||||
|
window: self, // Some modules expect window to be defined
|
||||||
|
global: self // Some modules expect global to be defined
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a function that will run in the proper context
|
||||||
|
const initFunction = new Function(
|
||||||
|
'module', 'exports', 'self', 'window', 'global',
|
||||||
|
jsQRCode
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute the function in our context
|
||||||
|
initFunction.call(
|
||||||
|
context,
|
||||||
|
context.module,
|
||||||
|
context.exports,
|
||||||
|
context.self,
|
||||||
|
context.window,
|
||||||
|
context.global
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the jsQR function - it might be exported directly or as default
|
||||||
|
self.jsQR = context.module.exports.default || context.module.exports;
|
||||||
|
|
||||||
|
// Verify the function works
|
||||||
|
if (typeof self.jsQR === 'function') {
|
||||||
|
// Test with a small dummy image
|
||||||
|
const testData = new Uint8ClampedArray(4 * 4 * 4);
|
||||||
|
self.jsQR(testData, 4, 4, { inversionAttempts: "dontInvert" });
|
||||||
|
|
||||||
|
jsQRInitialized = true;
|
||||||
|
self.postMessage({ initialized: true });
|
||||||
|
} else {
|
||||||
|
throw new Error('jsQR is not a function after initialization');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
initializationError = error;
|
||||||
|
console.error('Worker initialization error:', error);
|
||||||
|
self.postMessage({
|
||||||
|
error: 'Failed to initialize jsQR: ' + error.message,
|
||||||
|
details: {
|
||||||
|
errorType: error.name,
|
||||||
|
errorStack: error.stack,
|
||||||
|
jsQRType: typeof self.jsQR
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = function(e) {
|
||||||
|
if (!jsQRInitialized) {
|
||||||
|
self.postMessage({
|
||||||
|
success: false,
|
||||||
|
error: 'QR scanner not initialized',
|
||||||
|
details: {
|
||||||
|
initializationError: initializationError ? initializationError.message : 'Unknown error',
|
||||||
|
jsQRType: typeof self.jsQR
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { imageData, width, height } = e.data;
|
const { imageData, width, height } = e.data;
|
||||||
try {
|
try {
|
||||||
// Import jsQR in the worker
|
const uint8Array = new Uint8ClampedArray(imageData);
|
||||||
importScripts('${window.location.origin}/assets/jsqr.js');
|
|
||||||
const code = self.jsQR(imageData, width, height, {
|
// Try normal orientation
|
||||||
inversionAttempts: "dontInvert"
|
let code = self.jsQR(uint8Array, width, height, {
|
||||||
|
inversionAttempts: "attemptBoth"
|
||||||
});
|
});
|
||||||
self.postMessage({ success: true, code });
|
|
||||||
|
if (!code) {
|
||||||
|
// Try rotated 90 degrees
|
||||||
|
const rotated = rotateImageData(new ImageData(uint8Array, width, height), width, height);
|
||||||
|
if (rotated) {
|
||||||
|
code = self.jsQR(new Uint8ClampedArray(rotated.data), rotated.width, rotated.height, {
|
||||||
|
inversionAttempts: "attemptBoth"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postMessage(code ? { success: true, code } : { success: false, error: 'No QR code found' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({ success: false, error: error.message });
|
console.error('QR processing error:', error);
|
||||||
|
self.postMessage({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
details: {
|
||||||
|
errorType: error.name,
|
||||||
|
errorStack: error instanceof Error ? error.stack : undefined,
|
||||||
|
jsQRType: typeof self.jsQR
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
`], { type: 'text/javascript' })));
|
|
||||||
|
|
||||||
const image = new Image();
|
function rotateImageData(imageData, width, height) {
|
||||||
image.crossOrigin = 'anonymous';
|
const canvas = new OffscreenCanvas(height, width);
|
||||||
image.src = imageDataUrl;
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return null;
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(() => reject(new Error('Image load timeout')), 5000);
|
|
||||||
image.onload = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve(undefined);
|
|
||||||
};
|
|
||||||
image.onerror = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
reject(new Error('Failed to load image'));
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log('Image loaded, creating canvas...');
|
const newImageData = ctx.createImageData(height, width);
|
||||||
const canvas = document.createElement('canvas');
|
for (let y = 0; y < height; y++) {
|
||||||
const maxDimension = 1024; // Limit image size for better performance
|
for (let x = 0; x < width; x++) {
|
||||||
|
const srcIndex = (y * width + x) * 4;
|
||||||
// Scale down image if needed while maintaining aspect ratio
|
const destIndex = ((width - x - 1) * height + y) * 4;
|
||||||
let width = image.naturalWidth || 800;
|
newImageData.data[destIndex] = imageData.data[srcIndex];
|
||||||
let height = image.naturalHeight || 600;
|
newImageData.data[destIndex + 1] = imageData.data[srcIndex + 1];
|
||||||
if (width > maxDimension || height > maxDimension) {
|
newImageData.data[destIndex + 2] = imageData.data[srcIndex + 2];
|
||||||
if (width > height) {
|
newImageData.data[destIndex + 3] = imageData.data[srcIndex + 3];
|
||||||
height = Math.floor(height * (maxDimension / width));
|
}
|
||||||
width = maxDimension;
|
}
|
||||||
} else {
|
return { data: newImageData.data, width: height, height: width };
|
||||||
width = Math.floor(width * (maxDimension / height));
|
|
||||||
height = maxDimension;
|
|
||||||
}
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Terminate existing worker if it exists
|
||||||
|
if (this.worker) {
|
||||||
|
logger.log('Terminating existing worker');
|
||||||
|
this.worker.terminate();
|
||||||
|
this.worker = null;
|
||||||
|
this.workerInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.width = width;
|
// Create blob and worker
|
||||||
canvas.height = height;
|
logger.log('Creating worker blob and URL');
|
||||||
|
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
||||||
|
const workerUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
logger.log('Creating new worker');
|
||||||
if (!ctx) {
|
this.worker = new Worker(workerUrl);
|
||||||
throw new Error('Failed to get canvas context');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw image maintaining orientation
|
|
||||||
ctx.save();
|
|
||||||
ctx.drawImage(image, 0, 0, width, height);
|
|
||||||
ctx.restore();
|
|
||||||
|
|
||||||
const imageData = ctx.getImageData(0, 0, width, height);
|
|
||||||
|
|
||||||
logger.log('Processing image data for QR code...', {
|
// Wait for worker to be ready with increased timeout
|
||||||
width,
|
await new Promise<void>((resolve, reject) => {
|
||||||
height,
|
if (!this.worker) {
|
||||||
dataLength: imageData.data.length
|
reject(new Error('Worker failed to initialize'));
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Process QR code in worker
|
logger.log('Setting up worker message handlers');
|
||||||
const result = await new Promise<QRCodeResult>((resolve, reject) => {
|
const timeoutId = setTimeout(() => {
|
||||||
worker.onmessage = (e: MessageEvent<WorkerMessage>) => {
|
logger.error('Worker initialization timed out after 10 seconds');
|
||||||
if (e.data.success) {
|
if (this.worker) {
|
||||||
resolve(e.data.code);
|
this.worker.terminate();
|
||||||
} else {
|
this.worker = null;
|
||||||
|
}
|
||||||
|
reject(new Error('Worker initialization timed out'));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
this.worker.onmessage = (e) => {
|
||||||
|
logger.log('Received worker message:', e.data);
|
||||||
|
if (e.data?.initialized) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
this.workerInitialized = true;
|
||||||
|
resolve();
|
||||||
|
} else if (e.data?.error) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
reject(new Error(e.data.error));
|
reject(new Error(e.data.error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
worker.onerror = reject;
|
|
||||||
worker.postMessage({
|
this.worker.onerror = (error) => {
|
||||||
imageData: imageData.data,
|
logger.error('Worker error during initialization:', error);
|
||||||
width: imageData.width,
|
clearTimeout(timeoutId);
|
||||||
height: imageData.height
|
reject(new Error(`Worker error: ${error.message}`));
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
worker.terminate();
|
// Clean up the URL after worker is initialized
|
||||||
|
URL.revokeObjectURL(workerUrl);
|
||||||
if (result) {
|
logger.log('Worker initialized successfully');
|
||||||
logger.log('QR code found:', { data: result.data });
|
this.setProcessingStatus('Ready', 'QR scanner initialized');
|
||||||
await this.handleQRCodeResult(result.data);
|
|
||||||
} else {
|
|
||||||
logger.log('No QR code found in image');
|
|
||||||
this.showError('No QR code found. Please try again.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('QR code processing failed:', error);
|
logger.error('Failed to initialize worker:', error);
|
||||||
this.showError('Failed to process QR code. Please try again.');
|
this.setProcessingStatus('Error', `Failed to initialize QR scanner: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
|
||||||
|
if (this.worker) {
|
||||||
|
this.worker.terminate();
|
||||||
|
this.worker = null;
|
||||||
|
}
|
||||||
|
this.workerInitialized = false;
|
||||||
|
|
||||||
|
// Show appropriate error message based on attempt count
|
||||||
|
if (this.initializationAttempts >= this.MAX_INITIALIZATION_ATTEMPTS) {
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "error",
|
||||||
|
title: "QR Scanner Error",
|
||||||
|
text: "Failed to initialize QR scanner after multiple attempts. Please try again later.",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "QR Scanner Warning",
|
||||||
|
text: "Failed to initialize QR scanner. Retrying...",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadImage(imageDataUrl: string): Promise<HTMLImageElement> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.crossOrigin = 'anonymous';
|
||||||
|
|
||||||
|
const loadTimeout = setTimeout(() => {
|
||||||
|
img.src = '';
|
||||||
|
reject(new Error('Image load timeout'));
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
clearTimeout(loadTimeout);
|
||||||
|
resolve(img);
|
||||||
|
};
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
clearTimeout(loadTimeout);
|
||||||
|
reject(new Error('Failed to load image'));
|
||||||
|
};
|
||||||
|
|
||||||
|
img.src = imageDataUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getImageData(img: HTMLImageElement): { imageData: ImageData; width: number; height: number } {
|
||||||
|
const MAX_IMAGE_DIMENSION = 1024;
|
||||||
|
const aspectRatio = img.naturalWidth / img.naturalHeight;
|
||||||
|
|
||||||
|
// Calculate dimensions maintaining aspect ratio
|
||||||
|
let width = MAX_IMAGE_DIMENSION;
|
||||||
|
let height = MAX_IMAGE_DIMENSION;
|
||||||
|
|
||||||
|
if (aspectRatio > 1) {
|
||||||
|
height = Math.round(width / aspectRatio);
|
||||||
|
} else {
|
||||||
|
width = Math.round(height * aspectRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = new OffscreenCanvas(width, height);
|
||||||
|
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('Failed to get canvas context');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear canvas and ensure proper rendering
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
ctx.imageSmoothingEnabled = false;
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, width, height);
|
||||||
|
return { imageData, width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processImageForQRCode(imageDataUrl: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!this.worker || !this.workerInitialized) {
|
||||||
|
this.setProcessingStatus('Initializing', 'Setting up QR scanner...');
|
||||||
|
try {
|
||||||
|
await this.initializeWorker();
|
||||||
|
} catch (error) {
|
||||||
|
this.setProcessingStatus('Error', 'QR scanner initialization failed');
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "error",
|
||||||
|
title: "QR Scanner Error",
|
||||||
|
text: "Failed to initialize QR scanner. Please try again.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessing = true;
|
||||||
|
this.setProcessingStatus('Processing', 'Loading image...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const img = await this.loadImage(imageDataUrl);
|
||||||
|
const { imageData, width, height } = this.getImageData(img);
|
||||||
|
|
||||||
|
this.setProcessingStatus('Processing', 'Analyzing image for QR code...');
|
||||||
|
|
||||||
|
const result = await new Promise<WorkerResult>((resolve, reject) => {
|
||||||
|
if (!this.worker) {
|
||||||
|
reject(new Error('Worker not initialized'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
this.setProcessingStatus('Error', 'QR code processing timed out');
|
||||||
|
reject(new Error('QR code processing timed out'));
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
this.worker.onmessage = (e: MessageEvent<WorkerResult>) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
resolve(e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.worker.onerror = (error) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
this.setProcessingStatus('Error', `Processing error: ${error.message}`);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.setProcessingStatus('Processing', 'Scanning for QR code...');
|
||||||
|
this.worker.postMessage({
|
||||||
|
imageData: Array.from(imageData.data),
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success && result.code) {
|
||||||
|
this.setProcessingStatus('Success', 'QR code found!');
|
||||||
|
await this.handleQRCodeResult(result.code.data);
|
||||||
|
} else {
|
||||||
|
this.setProcessingStatus('Error', result.error || 'No QR code found');
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "No QR Code Found",
|
||||||
|
text: "Please make sure the QR code is clearly visible and try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('QR processing error:', error);
|
||||||
|
this.setProcessingStatus('Error', `Failed to process QR code: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "error",
|
||||||
|
title: "Processing Error",
|
||||||
|
text: "Failed to process the image. Please try again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.cameraActive = false;
|
this.isProcessing = false;
|
||||||
this.isCapturingPhoto = false;
|
this.isCapturingPhoto = false;
|
||||||
this.addCameraState('processing_completed');
|
this.addCameraState('processing_completed');
|
||||||
|
this.addCameraState('capture_completed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,9 +938,9 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.danger(
|
this.danger(
|
||||||
"Could not extract contact information from the QR code. Please try again.",
|
"Could not extract contact information from the QR code. Please try again.",
|
||||||
"Invalid QR Code",
|
"Invalid QR Code",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate JWT format
|
// Validate JWT format
|
||||||
if (
|
if (
|
||||||
@@ -1057,5 +1386,34 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.addCameraState('capture_completed');
|
this.addCameraState('capture_completed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setProcessingStatus(status: string, details = '') {
|
||||||
|
this.isProcessing = status !== 'Error' && status !== 'Success';
|
||||||
|
this.processingStatus = status;
|
||||||
|
this.processingDetails = details;
|
||||||
|
|
||||||
|
// Log the status change
|
||||||
|
this.logWithDetails('Processing status changed', {
|
||||||
|
status,
|
||||||
|
details,
|
||||||
|
isProcessing: this.isProcessing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private logWithDetails(message: string, details?: Record<string, unknown>) {
|
||||||
|
const formattedDetails = details ?
|
||||||
|
'\n' + JSON.stringify(details, (key, value) => {
|
||||||
|
if (value instanceof Error) {
|
||||||
|
return {
|
||||||
|
message: value.message,
|
||||||
|
stack: value.stack,
|
||||||
|
name: value.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}, 2) : '';
|
||||||
|
|
||||||
|
logger.log(`${message}${formattedDetails}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user