|
|
@ -2,6 +2,15 @@ |
|
|
|
<QuickNav selected="Profile" /> |
|
|
|
<!-- CONTENT --> |
|
|
|
<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 --> |
|
|
|
<div class="mb-8"> |
|
|
|
<!-- Back --> |
|
|
@ -202,6 +211,20 @@ interface 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({ |
|
|
|
components: { |
|
|
|
QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null, |
|
|
@ -238,42 +261,75 @@ export default class ContactQRScanShow extends Vue { |
|
|
|
private isCapturingPhoto = false; |
|
|
|
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() { |
|
|
|
logger.log('ContactQRScanShow component created'); |
|
|
|
try { |
|
|
|
// Remove any existing listeners first |
|
|
|
await App.removeAllListeners(); |
|
|
|
await this.cleanupAppListeners(); |
|
|
|
|
|
|
|
// Add app state listeners |
|
|
|
const stateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => { |
|
|
|
await this.setupAppLifecycleListeners(); |
|
|
|
|
|
|
|
// Load initial data |
|
|
|
await this.loadInitialData(); |
|
|
|
|
|
|
|
logger.log('ContactQRScanShow initialization complete'); |
|
|
|
} catch (error) { |
|
|
|
logger.error('Failed to initialize ContactQRScanShow:', error); |
|
|
|
this.showError('Failed to initialize. Please try again.'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
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.cameraActive) { |
|
|
|
if (!state.isActive) { |
|
|
|
this.cleanupCamera(); |
|
|
|
} |
|
|
|
}); |
|
|
|
this.appStateListener = stateListener; |
|
|
|
|
|
|
|
// Add pause listener |
|
|
|
await App.addListener('pause', () => { |
|
|
|
logger.log('App paused'); |
|
|
|
if (this.cameraActive) { |
|
|
|
this.cleanupCamera(); |
|
|
|
} |
|
|
|
this.cleanupCamera(); |
|
|
|
}); |
|
|
|
|
|
|
|
// Add resume listener |
|
|
|
await App.addListener('resume', () => { |
|
|
|
logger.log('App resumed'); |
|
|
|
if (this.cameraActive) { |
|
|
|
this.initializeCamera(); |
|
|
|
} |
|
|
|
// Don't automatically reinitialize camera on resume |
|
|
|
// Let user explicitly request camera access again |
|
|
|
}); |
|
|
|
|
|
|
|
// Load initial data |
|
|
|
await this.loadInitialData(); |
|
|
|
|
|
|
|
logger.log('ContactQRScanShow initialization complete'); |
|
|
|
logger.log('App lifecycle listeners setup complete'); |
|
|
|
} catch (error) { |
|
|
|
logger.error('Failed to initialize ContactQRScanShow:', error); |
|
|
|
this.showError('Failed to initialize. Please try again.'); |
|
|
|
logger.error('Error setting up app lifecycle listeners:', error); |
|
|
|
throw error; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -292,22 +348,34 @@ export default class ContactQRScanShow extends Vue { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private cleanupCamera() { |
|
|
|
private async cleanupCamera() { |
|
|
|
try { |
|
|
|
this.cameraActive = false; |
|
|
|
this.isCapturingPhoto = false; |
|
|
|
this.addCameraState('cleanup'); |
|
|
|
logger.log('Camera cleaned up successfully'); |
|
|
|
if (this.cameraActive) { |
|
|
|
this.cameraActive = false; |
|
|
|
this.isCapturingPhoto = false; |
|
|
|
this.addCameraState('cleanup'); |
|
|
|
logger.log('Camera cleaned up successfully'); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
logger.error('Error during camera cleanup:', error); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private addCameraState(state: CameraState | QRProcessingState, details?: Record<string, unknown>) { |
|
|
|
// Prevent duplicate state transitions |
|
|
|
if (this.lastCameraState === state) { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
const entry: CameraStateHistoryEntry = { |
|
|
|
state, |
|
|
|
timestamp: Date.now(), |
|
|
|
details |
|
|
|
details: { |
|
|
|
...details, |
|
|
|
cameraActive: this.cameraActive, |
|
|
|
isCapturingPhoto: this.isCapturingPhoto, |
|
|
|
isProcessing: this.isProcessing |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
this.cameraStateHistory.push(entry); |
|
|
@ -316,17 +384,11 @@ export default class ContactQRScanShow extends Vue { |
|
|
|
this.cameraStateHistory.shift(); |
|
|
|
} |
|
|
|
|
|
|
|
// Enhanced logging with better details |
|
|
|
logger.log('Camera state transition:', { |
|
|
|
state, |
|
|
|
details: { |
|
|
|
...details, |
|
|
|
cameraActive: this.cameraActive, |
|
|
|
isCapturingPhoto: this.isCapturingPhoto, |
|
|
|
historyLength: this.cameraStateHistory.length |
|
|
|
} |
|
|
|
this.logWithDetails('Camera state transition', { |
|
|
|
state: entry.state, |
|
|
|
timestamp: entry.timestamp, |
|
|
|
details: entry.details |
|
|
|
}); |
|
|
|
|
|
|
|
this.lastCameraState = state; |
|
|
|
} |
|
|
|
|
|
|
@ -363,6 +425,11 @@ export default class ContactQRScanShow extends Vue { |
|
|
|
]).catch(error => { |
|
|
|
logger.error('Error during component cleanup:', error); |
|
|
|
}); |
|
|
|
|
|
|
|
if (this.worker) { |
|
|
|
this.worker.terminate(); |
|
|
|
this.worker = null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
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 { |
|
|
|
logger.log('Starting QR code processing'); |
|
|
|
if (this.initializationAttempts >= this.MAX_INITIALIZATION_ATTEMPTS) { |
|
|
|
throw new Error('Maximum worker initialization attempts reached'); |
|
|
|
} |
|
|
|
|
|
|
|
this.initializationAttempts++; |
|
|
|
this.setProcessingStatus('Initializing', `Setting up QR scanner (attempt ${this.initializationAttempts})...`); |
|
|
|
logger.log('Starting worker initialization', { attempt: this.initializationAttempts }); |
|
|
|
|
|
|
|
// Create worker for image processing |
|
|
|
const worker = new Worker(URL.createObjectURL(new Blob([` |
|
|
|
self.onmessage = async function(e) { |
|
|
|
// 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; |
|
|
|
try { |
|
|
|
// Import jsQR in the worker |
|
|
|
importScripts('${window.location.origin}/assets/jsqr.js'); |
|
|
|
const code = self.jsQR(imageData, width, height, { |
|
|
|
inversionAttempts: "dontInvert" |
|
|
|
const uint8Array = new Uint8ClampedArray(imageData); |
|
|
|
|
|
|
|
// Try normal orientation |
|
|
|
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) { |
|
|
|
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(); |
|
|
|
image.crossOrigin = 'anonymous'; |
|
|
|
image.src = imageDataUrl; |
|
|
|
function rotateImageData(imageData, width, height) { |
|
|
|
const canvas = new OffscreenCanvas(height, width); |
|
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
if (!ctx) return null; |
|
|
|
|
|
|
|
const newImageData = ctx.createImageData(height, width); |
|
|
|
for (let y = 0; y < height; y++) { |
|
|
|
for (let x = 0; x < width; x++) { |
|
|
|
const srcIndex = (y * width + x) * 4; |
|
|
|
const destIndex = ((width - x - 1) * height + y) * 4; |
|
|
|
newImageData.data[destIndex] = imageData.data[srcIndex]; |
|
|
|
newImageData.data[destIndex + 1] = imageData.data[srcIndex + 1]; |
|
|
|
newImageData.data[destIndex + 2] = imageData.data[srcIndex + 2]; |
|
|
|
newImageData.data[destIndex + 3] = imageData.data[srcIndex + 3]; |
|
|
|
} |
|
|
|
} |
|
|
|
return { data: newImageData.data, width: height, height: width }; |
|
|
|
} |
|
|
|
`; |
|
|
|
|
|
|
|
// Terminate existing worker if it exists |
|
|
|
if (this.worker) { |
|
|
|
logger.log('Terminating existing worker'); |
|
|
|
this.worker.terminate(); |
|
|
|
this.worker = null; |
|
|
|
this.workerInitialized = false; |
|
|
|
} |
|
|
|
|
|
|
|
// Create blob and worker |
|
|
|
logger.log('Creating worker blob and URL'); |
|
|
|
const blob = new Blob([workerCode], { type: 'application/javascript' }); |
|
|
|
const workerUrl = URL.createObjectURL(blob); |
|
|
|
|
|
|
|
await new Promise((resolve, reject) => { |
|
|
|
const timeout = setTimeout(() => reject(new Error('Image load timeout')), 5000); |
|
|
|
image.onload = () => { |
|
|
|
clearTimeout(timeout); |
|
|
|
resolve(undefined); |
|
|
|
logger.log('Creating new worker'); |
|
|
|
this.worker = new Worker(workerUrl); |
|
|
|
|
|
|
|
// Wait for worker to be ready with increased timeout |
|
|
|
await new Promise<void>((resolve, reject) => { |
|
|
|
if (!this.worker) { |
|
|
|
reject(new Error('Worker failed to initialize')); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
logger.log('Setting up worker message handlers'); |
|
|
|
const timeoutId = setTimeout(() => { |
|
|
|
logger.error('Worker initialization timed out after 10 seconds'); |
|
|
|
if (this.worker) { |
|
|
|
this.worker.terminate(); |
|
|
|
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)); |
|
|
|
} |
|
|
|
}; |
|
|
|
image.onerror = () => { |
|
|
|
clearTimeout(timeout); |
|
|
|
reject(new Error('Failed to load image')); |
|
|
|
|
|
|
|
this.worker.onerror = (error) => { |
|
|
|
logger.error('Worker error during initialization:', error); |
|
|
|
clearTimeout(timeoutId); |
|
|
|
reject(new Error(`Worker error: ${error.message}`)); |
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
logger.log('Image loaded, creating canvas...'); |
|
|
|
const canvas = document.createElement('canvas'); |
|
|
|
const maxDimension = 1024; // Limit image size for better performance |
|
|
|
// Clean up the URL after worker is initialized |
|
|
|
URL.revokeObjectURL(workerUrl); |
|
|
|
logger.log('Worker initialized successfully'); |
|
|
|
this.setProcessingStatus('Ready', 'QR scanner initialized'); |
|
|
|
|
|
|
|
// Scale down image if needed while maintaining aspect ratio |
|
|
|
let width = image.naturalWidth || 800; |
|
|
|
let height = image.naturalHeight || 600; |
|
|
|
if (width > maxDimension || height > maxDimension) { |
|
|
|
if (width > height) { |
|
|
|
height = Math.floor(height * (maxDimension / width)); |
|
|
|
width = maxDimension; |
|
|
|
} else { |
|
|
|
width = Math.floor(width * (maxDimension / height)); |
|
|
|
height = maxDimension; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
canvas.width = width; |
|
|
|
canvas.height = height; |
|
|
|
} catch (error) { |
|
|
|
logger.error('Failed to initialize worker:', error); |
|
|
|
this.setProcessingStatus('Error', `Failed to initialize QR scanner: ${error instanceof Error ? error.message : String(error)}`); |
|
|
|
|
|
|
|
const ctx = canvas.getContext('2d'); |
|
|
|
if (!ctx) { |
|
|
|
throw new Error('Failed to get canvas context'); |
|
|
|
if (this.worker) { |
|
|
|
this.worker.terminate(); |
|
|
|
this.worker = null; |
|
|
|
} |
|
|
|
this.workerInitialized = false; |
|
|
|
|
|
|
|
// Draw image maintaining orientation |
|
|
|
ctx.save(); |
|
|
|
ctx.drawImage(image, 0, 0, width, height); |
|
|
|
ctx.restore(); |
|
|
|
// 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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, width, height); |
|
|
|
private async loadImage(imageDataUrl: string): Promise<HTMLImageElement> { |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
const img = new Image(); |
|
|
|
img.crossOrigin = 'anonymous'; |
|
|
|
|
|
|
|
logger.log('Processing image data for QR code...', { |
|
|
|
width, |
|
|
|
height, |
|
|
|
dataLength: imageData.data.length |
|
|
|
}); |
|
|
|
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; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Process QR code in worker |
|
|
|
const result = await new Promise<QRCodeResult>((resolve, reject) => { |
|
|
|
worker.onmessage = (e: MessageEvent<WorkerMessage>) => { |
|
|
|
if (e.data.success) { |
|
|
|
resolve(e.data.code); |
|
|
|
} else { |
|
|
|
reject(new Error(e.data.error)); |
|
|
|
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; |
|
|
|
} |
|
|
|
}; |
|
|
|
worker.onerror = reject; |
|
|
|
worker.postMessage({ |
|
|
|
imageData: imageData.data, |
|
|
|
width: imageData.width, |
|
|
|
height: imageData.height |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
worker.terminate(); |
|
|
|
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) { |
|
|
|
logger.log('QR code found:', { data: result.data }); |
|
|
|
await this.handleQRCodeResult(result.data); |
|
|
|
} else { |
|
|
|
logger.log('No QR code found in image'); |
|
|
|
this.showError('No QR code found. Please try again.'); |
|
|
|
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.", |
|
|
|
}); |
|
|
|
} |
|
|
|
} catch (error) { |
|
|
|
logger.error('QR code processing failed:', error); |
|
|
|
this.showError('Failed to process QR code. Please try again.'); |
|
|
|
} finally { |
|
|
|
this.cameraActive = false; |
|
|
|
this.isProcessing = false; |
|
|
|
this.isCapturingPhoto = false; |
|
|
|
this.addCameraState('processing_completed'); |
|
|
|
this.addCameraState('capture_completed'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -609,9 +938,9 @@ export default class ContactQRScanShow extends Vue { |
|
|
|
this.danger( |
|
|
|
"Could not extract contact information from the QR code. Please try again.", |
|
|
|
"Invalid QR Code", |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Validate JWT format |
|
|
|
if ( |
|
|
@ -1057,5 +1386,34 @@ export default class ContactQRScanShow extends Vue { |
|
|
|
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> |
|
|
|