refactor(QRScanner): improve camera handling and UI feedback
- Add detailed camera status and initialization feedback\n- Implement proper error handling with specific error messages\n- Add camera switching functionality with visual indicator\n- Improve TypeScript types with DetectionResult interface\n- Fix duplicate onError method with consolidated error handling\n- Add version display (v1.1.0)\n- Enhance UI with better status indicators and debug info\n- Clean up code formatting and improve maintainability
This commit is contained in:
@@ -8,10 +8,15 @@
|
||||
class="dialog relative bg-white rounded-lg shadow-xl max-w-lg w-full mx-4"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Scan QR Code</h3>
|
||||
<div
|
||||
class="p-4 border-b border-gray-200 flex justify-between items-center"
|
||||
>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Scan QR Code</h3>
|
||||
<span class="text-xs text-gray-500">v1.1.0</span>
|
||||
</div>
|
||||
<button
|
||||
class="absolute top-4 right-4 text-gray-400 hover:text-gray-500"
|
||||
class="text-gray-400 hover:text-gray-500"
|
||||
aria-label="Close dialog"
|
||||
@click="close"
|
||||
>
|
||||
@@ -38,39 +43,89 @@
|
||||
class="relative aspect-square"
|
||||
>
|
||||
<!-- Status Message -->
|
||||
<div
|
||||
<div
|
||||
class="absolute top-0 left-0 right-0 bg-black bg-opacity-50 text-white text-center py-2 z-10"
|
||||
>
|
||||
<p v-if="isInitializing">Initializing camera...</p>
|
||||
<p v-else-if="isScanning">Position QR code in the frame</p>
|
||||
<p v-else-if="error" class="text-red-300">{{ error }}</p>
|
||||
<p v-else>Ready to scan</p>
|
||||
<div
|
||||
v-if="isInitializing"
|
||||
class="flex items-center justify-center space-x-2"
|
||||
>
|
||||
<svg
|
||||
class="animate-spin h-5 w-5 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{{ initializationStatus }}</span>
|
||||
</div>
|
||||
<p
|
||||
v-else-if="isScanning"
|
||||
class="flex items-center justify-center space-x-2"
|
||||
>
|
||||
<span
|
||||
class="inline-block w-2 h-2 bg-green-500 rounded-full animate-pulse"
|
||||
></span>
|
||||
<span>Position QR code in the frame</span>
|
||||
</p>
|
||||
<p v-else-if="error" class="text-red-300">
|
||||
<span class="font-medium">Error:</span> {{ error }}
|
||||
</p>
|
||||
<p v-else class="flex items-center justify-center space-x-2">
|
||||
<span
|
||||
class="inline-block w-2 h-2 bg-blue-500 rounded-full"
|
||||
></span>
|
||||
<span>Ready to scan</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<qrcode-stream
|
||||
:camera="options?.camera === 'front' ? 'user' : 'environment'"
|
||||
:camera="preferredCamera"
|
||||
@decode="onDecode"
|
||||
@init="onInit"
|
||||
@detect="onDetect"
|
||||
@error="onError"
|
||||
@camera-on="onCameraOn"
|
||||
@camera-off="onCameraOff"
|
||||
/>
|
||||
|
||||
|
||||
<!-- Scanning Frame -->
|
||||
<div
|
||||
class="absolute inset-0 border-2"
|
||||
:class="{
|
||||
'border-blue-500': !error && !isScanning,
|
||||
'border-green-500 animate-pulse': isScanning,
|
||||
'border-red-500': error
|
||||
'border-red-500': error,
|
||||
}"
|
||||
style="opacity: 0.5; pointer-events: none;"
|
||||
style="opacity: 0.5; pointer-events: none"
|
||||
></div>
|
||||
|
||||
<!-- Debug Info -->
|
||||
<div
|
||||
class="absolute bottom-16 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center py-1"
|
||||
>
|
||||
Camera: {{ preferredCamera === "user" ? "Front" : "Back" }} |
|
||||
Status: {{ cameraStatus }}
|
||||
</div>
|
||||
|
||||
<!-- Camera Switch Button -->
|
||||
<button
|
||||
@click="toggleCamera"
|
||||
class="absolute bottom-4 right-4 bg-white rounded-full p-2 shadow-lg"
|
||||
title="Switch camera"
|
||||
@click="toggleCamera"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6 text-gray-600"
|
||||
@@ -157,6 +212,16 @@ interface ScanProps {
|
||||
options?: QRScannerOptions;
|
||||
}
|
||||
|
||||
interface DetectionResult {
|
||||
content?: string;
|
||||
location?: {
|
||||
topLeft: { x: number; y: number };
|
||||
topRight: { x: number; y: number };
|
||||
bottomLeft: { x: number; y: number };
|
||||
bottomRight: { x: number; y: number };
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
QrcodeStream,
|
||||
@@ -167,6 +232,9 @@ export default class QRScannerDialog extends Vue {
|
||||
@Prop({ type: Function }) onError?: ScanProps["onError"];
|
||||
@Prop({ type: Object }) options?: ScanProps["options"];
|
||||
|
||||
// Version
|
||||
readonly version = "1.1.0";
|
||||
|
||||
visible = true;
|
||||
error: string | null = null;
|
||||
useQRReader = __USE_QR_READER__;
|
||||
@@ -175,10 +243,12 @@ export default class QRScannerDialog extends Vue {
|
||||
__IS_MOBILE__ ||
|
||||
Capacitor.getPlatform() === "android" ||
|
||||
Capacitor.getPlatform() === "ios";
|
||||
|
||||
|
||||
isInitializing = true;
|
||||
isScanning = false;
|
||||
preferredCamera: 'user' | 'environment' = 'environment';
|
||||
preferredCamera: "user" | "environment" = "environment";
|
||||
initializationStatus = "Checking camera access...";
|
||||
cameraStatus = "Initializing";
|
||||
|
||||
created() {
|
||||
logger.log("QRScannerDialog platform detection:", {
|
||||
@@ -189,7 +259,9 @@ export default class QRScannerDialog extends Vue {
|
||||
isNativePlatform: this.isNativePlatform,
|
||||
userAgent: navigator.userAgent,
|
||||
mediaDevices: !!navigator.mediaDevices,
|
||||
getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||
getUserMedia: !!(
|
||||
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
|
||||
),
|
||||
});
|
||||
|
||||
// If on native platform, close immediately and don't initialize web scanner
|
||||
@@ -200,7 +272,6 @@ export default class QRScannerDialog extends Vue {
|
||||
}
|
||||
|
||||
async onInit(promise: Promise<void>): Promise<void> {
|
||||
// Don't initialize on mobile platforms
|
||||
if (this.isNativePlatform) {
|
||||
logger.log("Skipping web scanner initialization on native platform");
|
||||
return;
|
||||
@@ -208,16 +279,85 @@ export default class QRScannerDialog extends Vue {
|
||||
|
||||
this.isInitializing = true;
|
||||
this.error = null;
|
||||
logger.log("Initializing QR scanner...");
|
||||
|
||||
this.initializationStatus = "Checking camera access...";
|
||||
|
||||
try {
|
||||
// First check if mediaDevices API is available
|
||||
if (!navigator.mediaDevices) {
|
||||
throw new Error(
|
||||
"Camera API not available. Please ensure you're using HTTPS.",
|
||||
);
|
||||
}
|
||||
|
||||
logger.log("Starting QR scanner initialization...", {
|
||||
mediaDevices: !!navigator.mediaDevices,
|
||||
getUserMedia: !!(
|
||||
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
|
||||
),
|
||||
constraints: {
|
||||
video: true,
|
||||
facingMode: this.preferredCamera,
|
||||
},
|
||||
});
|
||||
|
||||
// Explicitly request camera permission first
|
||||
this.initializationStatus = "Requesting camera permission...";
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: this.preferredCamera,
|
||||
},
|
||||
});
|
||||
|
||||
// Stop the test stream immediately
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
|
||||
this.initializationStatus = "Camera permission granted...";
|
||||
logger.log("Camera permission granted");
|
||||
} catch (permissionError) {
|
||||
const error = permissionError as Error;
|
||||
logger.error("Camera permission error:", {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
});
|
||||
|
||||
if (
|
||||
error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError"
|
||||
) {
|
||||
throw new Error(
|
||||
"Camera access denied. Please grant camera permission and try again.",
|
||||
);
|
||||
} else if (
|
||||
error.name === "NotFoundError" ||
|
||||
error.name === "DevicesNotFoundError"
|
||||
) {
|
||||
throw new Error(
|
||||
"No camera found. Please ensure your device has a camera.",
|
||||
);
|
||||
} else if (
|
||||
error.name === "NotReadableError" ||
|
||||
error.name === "TrackStartError"
|
||||
) {
|
||||
throw new Error("Camera is in use by another application.");
|
||||
} else {
|
||||
throw new Error(`Camera error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Now initialize the QR scanner
|
||||
this.initializationStatus = "Starting QR scanner...";
|
||||
logger.log("Initializing QR scanner...");
|
||||
await promise;
|
||||
|
||||
this.isInitializing = false;
|
||||
this.cameraStatus = "Ready";
|
||||
logger.log("QR scanner initialized successfully");
|
||||
} catch (error) {
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
this.error = wrappedError.message;
|
||||
this.cameraStatus = "Error";
|
||||
if (this.onError) {
|
||||
this.onError(wrappedError);
|
||||
}
|
||||
@@ -225,28 +365,70 @@ export default class QRScannerDialog extends Vue {
|
||||
error: wrappedError.message,
|
||||
stack: wrappedError.stack,
|
||||
name: wrappedError.name,
|
||||
type: wrappedError.constructor.name,
|
||||
});
|
||||
} finally {
|
||||
this.isInitializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
onDetect(promise: Promise<any>): void {
|
||||
onCameraOn(): void {
|
||||
this.cameraStatus = "Active";
|
||||
logger.log("Camera turned on successfully");
|
||||
}
|
||||
|
||||
onCameraOff(): void {
|
||||
this.cameraStatus = "Off";
|
||||
logger.log("Camera turned off");
|
||||
}
|
||||
|
||||
onDetect(result: DetectionResult | Promise<DetectionResult>): void {
|
||||
this.isScanning = true;
|
||||
this.cameraStatus = "Detecting";
|
||||
logger.log("QR code detected, processing...");
|
||||
promise
|
||||
.then((result) => {
|
||||
logger.log("QR code processed successfully:", result);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Error processing QR code:", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
// Handle both promise and direct value cases
|
||||
const processResult = (detection: DetectionResult) => {
|
||||
try {
|
||||
logger.log("QR code processed successfully:", detection);
|
||||
} catch (error) {
|
||||
this.handleError(error);
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
});
|
||||
this.cameraStatus = "Active";
|
||||
}
|
||||
};
|
||||
|
||||
// Handle both promise and non-promise results
|
||||
if (result && typeof result.then === "function") {
|
||||
result
|
||||
.then(processResult)
|
||||
.catch((error: Error) => this.handleError(error))
|
||||
.finally(() => {
|
||||
this.isScanning = false;
|
||||
this.cameraStatus = "Active";
|
||||
});
|
||||
} else {
|
||||
processResult(result);
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(error: unknown): void {
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
this.error = wrappedError.message;
|
||||
this.cameraStatus = "Error";
|
||||
|
||||
if (this.onError) {
|
||||
this.onError(wrappedError);
|
||||
}
|
||||
|
||||
logger.error("QR scanner error:", {
|
||||
error: wrappedError.message,
|
||||
stack: wrappedError.stack,
|
||||
name: wrappedError.name,
|
||||
type: wrappedError.constructor.name,
|
||||
});
|
||||
}
|
||||
|
||||
onDecode(result: string): void {
|
||||
@@ -255,35 +437,13 @@ export default class QRScannerDialog extends Vue {
|
||||
this.onScan(result);
|
||||
this.close();
|
||||
} catch (error) {
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
this.error = wrappedError.message;
|
||||
if (this.onError) {
|
||||
this.onError(wrappedError);
|
||||
}
|
||||
logger.error("Error handling QR scan result:", {
|
||||
error: wrappedError.message,
|
||||
stack: wrappedError.stack,
|
||||
name: wrappedError.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onError(error: Error): void {
|
||||
this.isScanning = false;
|
||||
logger.error("QR scanner error:", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name,
|
||||
});
|
||||
this.error = error.message;
|
||||
if (this.onError) {
|
||||
this.onError(error);
|
||||
this.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
toggleCamera(): void {
|
||||
this.preferredCamera = this.preferredCamera === 'user' ? 'environment' : 'user';
|
||||
this.preferredCamera =
|
||||
this.preferredCamera === "user" ? "environment" : "user";
|
||||
}
|
||||
|
||||
retryScanning(): void {
|
||||
|
||||
Reference in New Issue
Block a user