refactor(qr-scanner): improve QR code scanning with Web Workers and enhanced error handling
- Move QR code processing to Web Worker for better performance - Add comprehensive camera state tracking and logging - Implement proper app lifecycle management - Add image size limits and optimization (max 1024px) - Add proper cleanup of resources (worker, timeouts, listeners) - Improve error handling with detailed error messages - Add TypeScript interfaces for worker messages and QR code results - Update Vite config to properly handle Capacitor builds
This commit is contained in:
7
.cursor/rules/reports.mdc
Normal file
7
.cursor/rules/reports.mdc
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
- make reports chronologically in paragraph form without using pronouns or references to people
|
||||
- use this git command to make a report:
|
||||
16
.cursor/rules/ts-cross-platform-rule.mdc
Normal file
16
.cursor/rules/ts-cross-platform-rule.mdc
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
- all cross platform builds need to conform to [PlatformService.ts](mdc:src/services/PlatformService.ts), and [PlatformServiceFactory.ts](mdc:src/services/PlatformServiceFactory.ts)
|
||||
- [CapacitorPlatformService.ts](mdc:src/services/platforms/CapacitorPlatformService.ts) is used for mobile both iOS and Android
|
||||
- [ElectronPlatformService.ts](mdc:src/services/platforms/ElectronPlatformService.ts) is used for cross-platform (Windows, MacOS, and Linux) desktop builds using Electron.
|
||||
- [WebPlatformService.ts](mdc:src/services/platforms/WebPlatformService.ts) is used for traditional web browsers and PWA (Progressive Web Applications)
|
||||
- [PyWebViewPlatformService.ts](mdc:src/services/platforms/PyWebViewPlatformService.ts) is used for handling a electron-like desktop application which can run Python
|
||||
- Vite is used to differentiate builds for platforms
|
||||
- @vite.config.mts is used for general configuration which uses environment variables to determine next actions
|
||||
- @vite.config.common.mts handles common features in vite builds.
|
||||
- [vite.config.capacitor.mts](mdc:vite.config.capacitor.mts) handles features of Vite builds for capacitor
|
||||
- [vite.config.electron.mts](mdc:vite.config.electron.mts) handles features of Vite builds for electron
|
||||
- [vite.config.web.mts](mdc:vite.config.web.mts) handles features of Vite builds for traditional web browsers and PWAs
|
||||
Binary file not shown.
7
package-lock.json
generated
7
package-lock.json
generated
@@ -57,6 +57,7 @@
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"localstorage-slim": "^2.7.0",
|
||||
"lru-cache": "^10.2.0",
|
||||
@@ -19845,6 +19846,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jsqr": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
|
||||
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/katex": {
|
||||
"version": "0.16.21",
|
||||
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz",
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"build:web": "vite build --config vite.config.web.mts",
|
||||
"electron:dev": "npm run build && electron dist-electron",
|
||||
"electron:start": "electron dist-electron",
|
||||
"build:android": "rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
||||
"clean:android": "adb uninstall app.timesafari.app || true",
|
||||
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
||||
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
||||
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
||||
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
||||
@@ -95,6 +96,7 @@
|
||||
"jdenticon": "^3.2.0",
|
||||
"js-generate-password": "^0.1.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsqr": "^1.4.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"localstorage-slim": "^2.7.0",
|
||||
"lru-cache": "^10.2.0",
|
||||
|
||||
34
src/types/jsqr.d.ts
vendored
Normal file
34
src/types/jsqr.d.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
declare module 'jsqr' {
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface QRLocation {
|
||||
topLeft: Point;
|
||||
topRight: Point;
|
||||
bottomLeft: Point;
|
||||
bottomRight: Point;
|
||||
topLeftFinder: Point;
|
||||
topRightFinder: Point;
|
||||
bottomLeftFinder: Point;
|
||||
bottomRightAlignment?: Point;
|
||||
}
|
||||
|
||||
interface QRCode {
|
||||
binaryData: number[];
|
||||
data: string;
|
||||
location: QRLocation;
|
||||
}
|
||||
|
||||
function jsQR(
|
||||
imageData: Uint8ClampedArray,
|
||||
width: number,
|
||||
height: number,
|
||||
options?: {
|
||||
inversionAttempts?: 'dontInvert' | 'onlyInvert' | 'attemptBoth';
|
||||
}
|
||||
): QRCode | null;
|
||||
|
||||
export default jsQR;
|
||||
}
|
||||
@@ -109,26 +109,99 @@ import { PlatformService } from "../services/PlatformService";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { db } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||
import {
|
||||
generateEndorserJwtUrlForAccount,
|
||||
isDid,
|
||||
register,
|
||||
setVisibilityUtil,
|
||||
} from "../libs/endorserServer";
|
||||
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
||||
import { retrieveAccountMetadata } from "../libs/util";
|
||||
import { Router } from "vue-router";
|
||||
import { logger } from "../utils/logger";
|
||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
||||
import { Camera, CameraResultType, CameraSource, ImageOptions, CameraPermissionState } from '@capacitor/camera';
|
||||
import { App } from '@capacitor/app';
|
||||
import jsQR from "jsqr";
|
||||
|
||||
// Declare global constants
|
||||
declare const __USE_QR_READER__: boolean;
|
||||
declare const __IS_MOBILE__: boolean;
|
||||
|
||||
|
||||
// Define all possible camera states
|
||||
type CameraState =
|
||||
| 'initializing'
|
||||
| 'ready'
|
||||
| 'error'
|
||||
| 'checking_permissions'
|
||||
| 'no_camera_capability'
|
||||
| 'permission_status_checked'
|
||||
| 'requesting_permission'
|
||||
| 'permission_requested'
|
||||
| 'permission_check_error'
|
||||
| 'camera_initialized'
|
||||
| 'camera_error'
|
||||
| 'opening_camera'
|
||||
| 'capture_already_in_progress'
|
||||
| 'photo_captured'
|
||||
| 'processing_photo'
|
||||
| 'no_image_data'
|
||||
| 'capture_error'
|
||||
| 'user_cancelled'
|
||||
| 'permission_error'
|
||||
| 'hardware_unavailable'
|
||||
| 'capture_completed'
|
||||
| 'cleanup'
|
||||
| 'processing_completed';
|
||||
|
||||
// Define all possible QR processing states
|
||||
type QRProcessingState =
|
||||
| 'processing_image'
|
||||
| 'qr_code_detected'
|
||||
| 'no_qr_code_found'
|
||||
| 'processing_error';
|
||||
|
||||
interface CameraStateHistoryEntry {
|
||||
state: CameraState | QRProcessingState;
|
||||
timestamp: number;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
||||
// Custom AppState type to match our needs
|
||||
interface CustomAppState {
|
||||
state: 'active' | 'inactive' | 'background' | 'foreground';
|
||||
}
|
||||
|
||||
interface AppStateChangeEvent {
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
// Define worker message types
|
||||
interface QRCodeResult {
|
||||
data: string;
|
||||
location: {
|
||||
topLeftCorner: { x: number; y: number };
|
||||
topRightCorner: { x: number; y: number };
|
||||
bottomRightCorner: { x: number; y: number };
|
||||
bottomLeftCorner: { x: number; y: number };
|
||||
};
|
||||
}
|
||||
|
||||
interface WorkerSuccessMessage {
|
||||
success: true;
|
||||
code: QRCodeResult;
|
||||
}
|
||||
|
||||
interface WorkerErrorMessage {
|
||||
success: false;
|
||||
error: string;
|
||||
}
|
||||
|
||||
type WorkerMessage = WorkerSuccessMessage | WorkerErrorMessage;
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null,
|
||||
@@ -158,158 +231,339 @@ export default class ContactQRScanShow extends Vue {
|
||||
private platformService: PlatformService =
|
||||
PlatformServiceFactory.getInstance();
|
||||
|
||||
private cameraActive = false;
|
||||
private lastCameraState: CameraState | QRProcessingState = 'initializing';
|
||||
private cameraStateHistory: CameraStateHistoryEntry[] = [];
|
||||
private readonly STATE_HISTORY_LIMIT = 20;
|
||||
private isCapturingPhoto = false;
|
||||
private appStateListener?: { remove: () => Promise<void> };
|
||||
|
||||
async created() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.givenName = settings.firstName || "";
|
||||
this.hideRegisterPromptOnNewContact =
|
||||
!!settings.hideRegisterPromptOnNewContact;
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
logger.log('ContactQRScanShow component created');
|
||||
try {
|
||||
// Remove any existing listeners first
|
||||
await App.removeAllListeners();
|
||||
|
||||
const account = await retrieveAccountMetadata(this.activeDid);
|
||||
if (account) {
|
||||
const name =
|
||||
(settings.firstName || "") +
|
||||
(settings.lastName ? ` ${settings.lastName}` : "");
|
||||
// Add app state listeners
|
||||
const stateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
|
||||
logger.log('App state changed:', state);
|
||||
if (!state.isActive && this.cameraActive) {
|
||||
this.cleanupCamera();
|
||||
}
|
||||
});
|
||||
this.appStateListener = stateListener;
|
||||
|
||||
this.qrValue = await generateEndorserJwtUrlForAccount(
|
||||
account,
|
||||
!!settings.isRegistered,
|
||||
name,
|
||||
settings.profileImageUrl || "",
|
||||
false,
|
||||
);
|
||||
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
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize camera with retry logic
|
||||
if (this.useQRReader) {
|
||||
await this.initializeCamera();
|
||||
private async loadInitialData() {
|
||||
try {
|
||||
// Load settings from DB
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
if (settings) {
|
||||
this.hideRegisterPromptOnNewContact = settings.hideRegisterPromptOnNewContact || false;
|
||||
}
|
||||
logger.log('Initial data loaded successfully');
|
||||
} catch (error) {
|
||||
logger.error('Failed to load initial data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private cleanupCamera() {
|
||||
try {
|
||||
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>) {
|
||||
const entry: CameraStateHistoryEntry = {
|
||||
state,
|
||||
timestamp: Date.now(),
|
||||
details
|
||||
};
|
||||
|
||||
this.cameraStateHistory.push(entry);
|
||||
|
||||
if (this.cameraStateHistory.length > this.STATE_HISTORY_LIMIT) {
|
||||
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.lastCameraState = state;
|
||||
}
|
||||
|
||||
beforeDestroy() {
|
||||
logger.log('ContactQRScanShow component being destroyed, initiating cleanup', {
|
||||
cameraActive: this.cameraActive,
|
||||
lastState: this.lastCameraState,
|
||||
stateHistory: this.cameraStateHistory
|
||||
});
|
||||
|
||||
// Remove all app lifecycle listeners
|
||||
const cleanupListeners = async () => {
|
||||
if (this.appStateListener) {
|
||||
try {
|
||||
await this.appStateListener.remove();
|
||||
logger.log('App state change listener removed successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error removing app state change listener:', error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await App.removeAllListeners();
|
||||
logger.log('All app listeners removed successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error removing all app listeners:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup everything
|
||||
Promise.all([
|
||||
cleanupListeners(),
|
||||
this.cleanupCamera()
|
||||
]).catch(error => {
|
||||
logger.error('Error during component cleanup:', error);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleQRCodeResult(data: string): Promise<void> {
|
||||
try {
|
||||
await this.onScanDetect([{ rawValue: data }]);
|
||||
} catch (error) {
|
||||
console.error('Failed to handle QR code result:', error);
|
||||
this.showError('Failed to process QR code data.');
|
||||
}
|
||||
}
|
||||
|
||||
async initializeCamera(retryCount = 0): Promise<void> {
|
||||
try {
|
||||
const capabilities = this.platformService.getCapabilities();
|
||||
if (!capabilities.hasCamera) {
|
||||
this.danger("No camera available on this device.", "Camera Error");
|
||||
if (this.cameraActive) {
|
||||
console.log('Camera already active, skipping initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check camera permissions
|
||||
const hasPermission = await this.checkCameraPermission();
|
||||
if (!hasPermission) {
|
||||
this.danger(
|
||||
"Camera permission is required to scan QR codes. Please enable camera access in your device settings.",
|
||||
"Permission Required"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here, camera should be available
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Camera Ready",
|
||||
text: "Camera is ready to scan QR codes.",
|
||||
},
|
||||
3000
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error initializing camera:", error);
|
||||
|
||||
// Retry up to 3 times for certain errors
|
||||
if (retryCount < 3) {
|
||||
const isPermissionError = error instanceof Error &&
|
||||
(error.message.includes("permission") ||
|
||||
error.message.includes("NotReadableError"));
|
||||
|
||||
if (isPermissionError) {
|
||||
// Wait before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return this.initializeCamera(retryCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.danger(
|
||||
"Failed to initialize camera. Please check your camera permissions and try again.",
|
||||
"Camera Error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async checkCameraPermission(): Promise<boolean> {
|
||||
try {
|
||||
const capabilities = this.platformService.getCapabilities();
|
||||
if (!capabilities.hasCamera) {
|
||||
return false;
|
||||
console.log('Initializing camera...', { retryCount });
|
||||
|
||||
// Check camera permissions first
|
||||
const permissionStatus = await this.checkCameraPermission();
|
||||
if (permissionStatus.camera !== 'granted') {
|
||||
throw new Error('Camera permission not granted');
|
||||
}
|
||||
|
||||
// Try to access camera to check permissions
|
||||
await this.platformService.takePicture();
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Camera permission check failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.cameraActive = true;
|
||||
|
||||
async openMobileCamera(): Promise<void> {
|
||||
try {
|
||||
// Check permissions first
|
||||
const hasPermission = await this.checkCameraPermission();
|
||||
if (!hasPermission) {
|
||||
this.danger(
|
||||
"Camera permission is required. Please enable camera access in your device settings.",
|
||||
"Permission Required"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const image = await Camera.getPhoto({
|
||||
// Configure camera options
|
||||
const cameraOptions: ImageOptions = {
|
||||
quality: 90,
|
||||
allowEditing: false,
|
||||
resultType: CameraResultType.DataUrl,
|
||||
source: CameraSource.Camera,
|
||||
source: CameraSource.Camera
|
||||
};
|
||||
|
||||
console.log('Opening camera with options:', cameraOptions);
|
||||
const image = await Camera.getPhoto(cameraOptions);
|
||||
|
||||
if (!image || !image.dataUrl) {
|
||||
throw new Error('Failed to capture photo: No image data received');
|
||||
}
|
||||
|
||||
console.log('Photo captured successfully, processing image...');
|
||||
await this.processImageForQRCode(image.dataUrl);
|
||||
|
||||
} catch (error) {
|
||||
this.cameraActive = false;
|
||||
console.error('Camera initialization failed:', error instanceof Error ? error.message : String(error));
|
||||
|
||||
// Handle user cancellation separately
|
||||
if (error instanceof Error && error.message.includes('User cancelled photos app')) {
|
||||
console.log('User cancelled photo capture');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle retry logic
|
||||
if (retryCount < 2) {
|
||||
console.log('Retrying camera initialization...');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
return this.initializeCamera(retryCount + 1);
|
||||
}
|
||||
|
||||
this.showError('Failed to initialize camera. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
async checkCameraPermission(): Promise<{ camera: CameraPermissionState }> {
|
||||
try {
|
||||
this.addCameraState('checking_permissions');
|
||||
const capabilities = this.platformService.getCapabilities();
|
||||
if (!capabilities.hasCamera) {
|
||||
this.addCameraState('no_camera_capability');
|
||||
return { camera: 'denied' as CameraPermissionState };
|
||||
}
|
||||
|
||||
const permissionStatus = await Camera.checkPermissions();
|
||||
this.addCameraState('permission_status_checked');
|
||||
|
||||
if (permissionStatus.camera === 'prompt') {
|
||||
this.addCameraState('requesting_permission');
|
||||
const requestResult = await Camera.requestPermissions();
|
||||
this.addCameraState('permission_requested');
|
||||
return requestResult;
|
||||
}
|
||||
|
||||
return permissionStatus;
|
||||
} catch (error) {
|
||||
this.addCameraState('permission_check_error');
|
||||
return { camera: 'denied' as CameraPermissionState };
|
||||
}
|
||||
}
|
||||
|
||||
async processImageForQRCode(imageDataUrl: string): Promise<void> {
|
||||
try {
|
||||
logger.log('Starting QR code processing');
|
||||
|
||||
// Create worker for image processing
|
||||
const worker = new Worker(URL.createObjectURL(new Blob([`
|
||||
self.onmessage = async function(e) {
|
||||
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"
|
||||
});
|
||||
self.postMessage({ success: true, code });
|
||||
} catch (error) {
|
||||
self.postMessage({ success: false, error: error.message });
|
||||
}
|
||||
};
|
||||
`], { type: 'text/javascript' })));
|
||||
|
||||
const image = new Image();
|
||||
image.crossOrigin = 'anonymous';
|
||||
image.src = imageDataUrl;
|
||||
|
||||
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'));
|
||||
};
|
||||
});
|
||||
|
||||
if (image.dataUrl) {
|
||||
await this.processImageForQRCode(image.dataUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error taking picture:", error);
|
||||
this.danger(
|
||||
"Failed to access camera. Please check your camera permissions.",
|
||||
"Camera Error"
|
||||
);
|
||||
logger.log('Image loaded, creating canvas...');
|
||||
const canvas = document.createElement('canvas');
|
||||
const maxDimension = 1024; // Limit image size for better performance
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
async processImageForQRCode(_imageDataUrl: string) {
|
||||
try {
|
||||
// Here you would implement QR code scanning from the image
|
||||
// For example, using jsQR:
|
||||
// const image = new Image();
|
||||
// image.src = imageDataUrl;
|
||||
// image.onload = () => {
|
||||
// const canvas = document.createElement('canvas');
|
||||
// const context = canvas.getContext('2d');
|
||||
// canvas.width = image.width;
|
||||
// canvas.height = image.height;
|
||||
// context.drawImage(image, 0, 0);
|
||||
// const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
// const code = jsQR(imageData.data, imageData.width, imageData.height);
|
||||
// if (code) {
|
||||
// this.onScanDetect([{ rawValue: code.data }]);
|
||||
// }
|
||||
// };
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
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...', {
|
||||
width,
|
||||
height,
|
||||
dataLength: imageData.data.length
|
||||
});
|
||||
|
||||
// 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));
|
||||
}
|
||||
};
|
||||
worker.onerror = reject;
|
||||
worker.postMessage({
|
||||
imageData: imageData.data,
|
||||
width: imageData.width,
|
||||
height: imageData.height
|
||||
});
|
||||
});
|
||||
|
||||
worker.terminate();
|
||||
|
||||
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.');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error processing image for QR code:", error);
|
||||
this.danger(
|
||||
"Failed to process the image. Please try again.",
|
||||
"Processing Error",
|
||||
);
|
||||
logger.error('QR code processing failed:', error);
|
||||
this.showError('Failed to process QR code. Please try again.');
|
||||
} finally {
|
||||
this.cameraActive = false;
|
||||
this.isCapturingPhoto = false;
|
||||
this.addCameraState('processing_completed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,5 +966,96 @@ export default class ContactQRScanShow extends Vue {
|
||||
get isMobile(): boolean {
|
||||
return __IS_MOBILE__;
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
// Implement the logic to show a user-friendly error message to the user
|
||||
console.error(message);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
|
||||
async openMobileCamera() {
|
||||
if (this.isCapturingPhoto) {
|
||||
this.addCameraState('capture_already_in_progress');
|
||||
logger.warn('Camera capture already in progress, ignoring request');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.isCapturingPhoto = true;
|
||||
this.addCameraState('opening_camera');
|
||||
|
||||
const config = {
|
||||
quality: 90,
|
||||
allowEditing: false,
|
||||
resultType: CameraResultType.DataUrl,
|
||||
source: CameraSource.Camera
|
||||
};
|
||||
|
||||
logger.log('Camera configuration:', config);
|
||||
|
||||
const image = await Camera.getPhoto(config);
|
||||
|
||||
this.addCameraState('photo_captured', {
|
||||
hasDataUrl: !!image.dataUrl,
|
||||
dataUrlLength: image.dataUrl?.length,
|
||||
format: image.format,
|
||||
saved: image.saved,
|
||||
webPath: image.webPath,
|
||||
base64String: image.dataUrl?.substring(0, 50) + '...' // Log first 50 chars of base64
|
||||
});
|
||||
|
||||
if (image.dataUrl) {
|
||||
this.addCameraState('processing_photo');
|
||||
await this.processImageForQRCode(image.dataUrl);
|
||||
} else {
|
||||
this.addCameraState('no_image_data');
|
||||
logger.error('Camera returned no image data');
|
||||
this.$notify({
|
||||
type: 'error',
|
||||
title: 'Camera Error',
|
||||
text: 'No image was captured. Please try again.',
|
||||
group: 'qr-scanner'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.addCameraState('capture_error', {
|
||||
error,
|
||||
errorName: error instanceof Error ? error.name : 'Unknown',
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
errorStack: error instanceof Error ? error.stack : undefined
|
||||
});
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('User cancelled photos app')) {
|
||||
logger.log('User cancelled photo capture');
|
||||
this.addCameraState('user_cancelled');
|
||||
} else if (error.message.includes('permission')) {
|
||||
logger.error('Camera permission error during capture');
|
||||
this.addCameraState('permission_error');
|
||||
} else if (error.message.includes('Camera is not available')) {
|
||||
logger.error('Camera hardware not available');
|
||||
this.addCameraState('hardware_unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
this.$notify({
|
||||
type: 'error',
|
||||
title: 'Camera Error',
|
||||
text: 'Failed to capture photo. Please check camera permissions and try again.',
|
||||
group: 'qr-scanner'
|
||||
});
|
||||
} finally {
|
||||
this.isCapturingPhoto = false;
|
||||
this.addCameraState('capture_completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.*"]
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { defineConfig, mergeConfig } from "vite";
|
||||
import { createBuildConfig } from "./vite.config.common.mts";
|
||||
|
||||
export default defineConfig(async () => createBuildConfig('capacitor'));
|
||||
export default defineConfig(async () => {
|
||||
const baseConfig = await createBuildConfig('capacitor');
|
||||
|
||||
return mergeConfig(baseConfig, {
|
||||
define: {
|
||||
__USE_QR_READER__: false, // Disable web QR reader on mobile
|
||||
__IS_MOBILE__: true, // Enable mobile-specific features
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ export async function createBuildConfig(mode: string) {
|
||||
assetsDir: 'assets',
|
||||
chunkSizeWarningLimit: 1000,
|
||||
rollupOptions: {
|
||||
external: isCapacitor ? ['@capacitor/app'] : []
|
||||
external: []
|
||||
}
|
||||
},
|
||||
define: {
|
||||
@@ -44,10 +44,13 @@ export async function createBuildConfig(mode: string) {
|
||||
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
|
||||
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
|
||||
__USE_QR_READER__: JSON.stringify(!isCapacitor),
|
||||
__IS_MOBILE__: JSON.stringify(isCapacitor),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@capacitor/app': path.resolve(__dirname, 'node_modules/@capacitor/app'),
|
||||
...appConfig.aliasConfig,
|
||||
'nostr-tools/nip06': mode === 'development'
|
||||
? 'nostr-tools/nip06'
|
||||
|
||||
Reference in New Issue
Block a user