forked from trent_larson/crowd-funder-for-time-pwa
style: improve code formatting and readability
- Format Vue template attributes and event handlers for better readability - Reorganize component props and event bindings - Improve error handling and state management in QR scanner - Add proper aria labels and accessibility attributes - Refactor camera state handling in WebInlineQRScanner - Clean up promise handling in WebPlatformService - Standardize string quotes to double quotes - Improve component structure and indentation No functional changes, purely code style and maintainability improvements.
This commit is contained in:
@@ -51,7 +51,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-center text-lg text-slate-500 py-12">Register to Upload a Photo</div>
|
||||
<div class="text-center text-lg text-slate-500 py-12">
|
||||
Register to Upload a Photo
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -173,12 +173,12 @@ export default class PhotoDialog extends Vue {
|
||||
* @throws {Error} When settings retrieval fails
|
||||
*/
|
||||
async mounted() {
|
||||
console.log('PhotoDialog mounted');
|
||||
logger.log("PhotoDialog mounted");
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
console.log('isRegistered:', this.isRegistered);
|
||||
logger.log("isRegistered:", this.isRegistered);
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error retrieving settings from database:", error);
|
||||
this.$notify(
|
||||
@@ -245,7 +245,10 @@ export default class PhotoDialog extends Vue {
|
||||
* Closes the photo dialog and resets state
|
||||
*/
|
||||
close() {
|
||||
logger.debug("Dialog closing, current showCameraPreview:", this.showCameraPreview);
|
||||
logger.debug(
|
||||
"Dialog closing, current showCameraPreview:",
|
||||
this.showCameraPreview,
|
||||
);
|
||||
this.visible = false;
|
||||
this.stopCameraPreview();
|
||||
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
||||
@@ -291,10 +294,13 @@ export default class PhotoDialog extends Vue {
|
||||
// Set state before requesting camera access
|
||||
this.showCameraPreview = true;
|
||||
logger.debug("showCameraPreview set to:", this.showCameraPreview);
|
||||
|
||||
|
||||
// Force a re-render
|
||||
await this.$nextTick();
|
||||
logger.debug("After nextTick, showCameraPreview is:", this.showCameraPreview);
|
||||
logger.debug(
|
||||
"After nextTick, showCameraPreview is:",
|
||||
this.showCameraPreview,
|
||||
);
|
||||
|
||||
logger.debug("Requesting camera access...");
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
@@ -302,10 +308,13 @@ export default class PhotoDialog extends Vue {
|
||||
});
|
||||
logger.debug("Camera access granted, setting up video element");
|
||||
this.cameraStream = stream;
|
||||
|
||||
|
||||
// Force another re-render after getting the stream
|
||||
await this.$nextTick();
|
||||
logger.debug("After getting stream, showCameraPreview is:", this.showCameraPreview);
|
||||
logger.debug(
|
||||
"After getting stream, showCameraPreview is:",
|
||||
this.showCameraPreview,
|
||||
);
|
||||
|
||||
const videoElement = this.$refs.videoElement as HTMLVideoElement;
|
||||
if (videoElement) {
|
||||
@@ -343,13 +352,19 @@ export default class PhotoDialog extends Vue {
|
||||
* Stops the camera preview and cleans up resources
|
||||
*/
|
||||
stopCameraPreview() {
|
||||
logger.debug("Stopping camera preview, current showCameraPreview:", this.showCameraPreview);
|
||||
logger.debug(
|
||||
"Stopping camera preview, current showCameraPreview:",
|
||||
this.showCameraPreview,
|
||||
);
|
||||
if (this.cameraStream) {
|
||||
this.cameraStream.getTracks().forEach((track) => track.stop());
|
||||
this.cameraStream = null;
|
||||
}
|
||||
this.showCameraPreview = false;
|
||||
logger.debug("After stopping, showCameraPreview is:", this.showCameraPreview);
|
||||
logger.debug(
|
||||
"After stopping, showCameraPreview is:",
|
||||
this.showCameraPreview,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,13 +381,17 @@ export default class PhotoDialog extends Vue {
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
this.blob = blob;
|
||||
this.fileName = `photo_${Date.now()}.jpg`;
|
||||
this.stopCameraPreview();
|
||||
}
|
||||
}, "image/jpeg", 0.95);
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
this.blob = blob;
|
||||
this.fileName = `photo_${Date.now()}.jpg`;
|
||||
this.stopCameraPreview();
|
||||
}
|
||||
},
|
||||
"image/jpeg",
|
||||
0.95,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error capturing photo:", error);
|
||||
this.$notify(
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { QRScannerService, ScanListener, QRScannerOptions, CameraState, CameraStateListener } from "./types";
|
||||
import {
|
||||
QRScannerService,
|
||||
ScanListener,
|
||||
QRScannerOptions,
|
||||
CameraState,
|
||||
CameraStateListener,
|
||||
} from "./types";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { EventEmitter } from "events";
|
||||
import jsQR from "jsqr";
|
||||
@@ -22,7 +28,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames
|
||||
private lastFrameTime = 0;
|
||||
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
||||
private currentState: CameraState = 'off';
|
||||
private currentState: CameraState = "off";
|
||||
private currentStateMessage?: string;
|
||||
|
||||
constructor(private options?: QRScannerOptions) {
|
||||
@@ -49,15 +55,21 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
private updateCameraState(state: CameraState, message?: string) {
|
||||
this.currentState = state;
|
||||
this.currentStateMessage = message;
|
||||
this.cameraStateListeners.forEach(listener => {
|
||||
this.cameraStateListeners.forEach((listener) => {
|
||||
try {
|
||||
listener.onStateChange(state, message);
|
||||
logger.info(`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`, {
|
||||
state,
|
||||
message,
|
||||
});
|
||||
logger.info(
|
||||
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
|
||||
{
|
||||
state,
|
||||
message,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Error in camera state listener:`, error);
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Error in camera state listener:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -74,7 +86,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
async checkPermissions(): Promise<boolean> {
|
||||
try {
|
||||
this.updateCameraState('initializing', 'Checking camera permissions...');
|
||||
this.updateCameraState("initializing", "Checking camera permissions...");
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
||||
);
|
||||
@@ -86,7 +98,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
permissions.state,
|
||||
);
|
||||
const granted = permissions.state === "granted";
|
||||
this.updateCameraState(granted ? 'ready' : 'permission_denied');
|
||||
this.updateCameraState(granted ? "ready" : "permission_denied");
|
||||
return granted;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -96,14 +108,17 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
},
|
||||
);
|
||||
this.updateCameraState('error', 'Error checking camera permissions');
|
||||
this.updateCameraState("error", "Error checking camera permissions");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async requestPermissions(): Promise<boolean> {
|
||||
try {
|
||||
this.updateCameraState('initializing', 'Requesting camera permissions...');
|
||||
this.updateCameraState(
|
||||
"initializing",
|
||||
"Requesting camera permissions...",
|
||||
);
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
||||
);
|
||||
@@ -141,8 +156,8 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
},
|
||||
});
|
||||
|
||||
this.updateCameraState('ready', 'Camera permissions granted');
|
||||
|
||||
this.updateCameraState("ready", "Camera permissions granted");
|
||||
|
||||
// Stop the test stream immediately
|
||||
stream.getTracks().forEach((track) => {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||
@@ -154,20 +169,35 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const wrappedError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
// Update state based on error type
|
||||
if (wrappedError.name === "NotFoundError" || wrappedError.name === "DevicesNotFoundError") {
|
||||
this.updateCameraState('not_found', 'No camera found on this device');
|
||||
if (
|
||||
wrappedError.name === "NotFoundError" ||
|
||||
wrappedError.name === "DevicesNotFoundError"
|
||||
) {
|
||||
this.updateCameraState("not_found", "No camera found on this device");
|
||||
throw new Error("No camera found on this device");
|
||||
} else if (wrappedError.name === "NotAllowedError" || wrappedError.name === "PermissionDeniedError") {
|
||||
this.updateCameraState('permission_denied', 'Camera access denied');
|
||||
throw new Error("Camera access denied. Please grant camera permission and try again");
|
||||
} else if (wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError") {
|
||||
this.updateCameraState('in_use', 'Camera is in use by another application');
|
||||
} else if (
|
||||
wrappedError.name === "NotAllowedError" ||
|
||||
wrappedError.name === "PermissionDeniedError"
|
||||
) {
|
||||
this.updateCameraState("permission_denied", "Camera access denied");
|
||||
throw new Error(
|
||||
"Camera access denied. Please grant camera permission and try again",
|
||||
);
|
||||
} else if (
|
||||
wrappedError.name === "NotReadableError" ||
|
||||
wrappedError.name === "TrackStartError"
|
||||
) {
|
||||
this.updateCameraState(
|
||||
"in_use",
|
||||
"Camera is in use by another application",
|
||||
);
|
||||
throw new Error("Camera is in use by another application");
|
||||
} else {
|
||||
this.updateCameraState('error', wrappedError.message);
|
||||
this.updateCameraState("error", wrappedError.message);
|
||||
throw new Error(`Camera error: ${wrappedError.message}`);
|
||||
}
|
||||
}
|
||||
@@ -406,7 +436,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.isScanning = true;
|
||||
this.scanAttempts = 0;
|
||||
this.lastScanTime = Date.now();
|
||||
this.updateCameraState('initializing', 'Starting camera...');
|
||||
this.updateCameraState("initializing", "Starting camera...");
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`);
|
||||
|
||||
// Get camera stream
|
||||
@@ -421,8 +451,8 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
},
|
||||
});
|
||||
|
||||
this.updateCameraState('active', 'Camera is active');
|
||||
|
||||
this.updateCameraState("active", "Camera is active");
|
||||
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
||||
tracks: this.stream.getTracks().map((t) => ({
|
||||
kind: t.kind,
|
||||
@@ -448,15 +478,22 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.scanQRCode();
|
||||
} catch (error) {
|
||||
this.isScanning = false;
|
||||
const wrappedError = error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
const wrappedError =
|
||||
error instanceof Error ? error : new Error(String(error));
|
||||
|
||||
// Update state based on error type
|
||||
if (wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError") {
|
||||
this.updateCameraState('in_use', 'Camera is in use by another application');
|
||||
if (
|
||||
wrappedError.name === "NotReadableError" ||
|
||||
wrappedError.name === "TrackStartError"
|
||||
) {
|
||||
this.updateCameraState(
|
||||
"in_use",
|
||||
"Camera is in use by another application",
|
||||
);
|
||||
} else {
|
||||
this.updateCameraState('error', wrappedError.message);
|
||||
this.updateCameraState("error", wrappedError.message);
|
||||
}
|
||||
|
||||
|
||||
if (this.scanListener?.onError) {
|
||||
this.scanListener.onError(wrappedError);
|
||||
}
|
||||
@@ -513,8 +550,11 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Error stopping scan:`, error);
|
||||
this.updateCameraState('error', 'Error stopping camera');
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Error stopping scan:`,
|
||||
error,
|
||||
);
|
||||
this.updateCameraState("error", "Error stopping camera");
|
||||
throw error;
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
@@ -557,8 +597,11 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Error during cleanup:`, error);
|
||||
this.updateCameraState('error', 'Error during cleanup');
|
||||
logger.error(
|
||||
`[WebInlineQRScanner:${this.id}] Error during cleanup:`,
|
||||
error,
|
||||
);
|
||||
this.updateCameraState("error", "Error during cleanup");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,15 +22,15 @@ export interface QRScannerOptions {
|
||||
playSound?: boolean;
|
||||
}
|
||||
|
||||
export type CameraState =
|
||||
| 'initializing' // Camera is being initialized
|
||||
| 'ready' // Camera is ready to use
|
||||
| 'active' // Camera is actively streaming
|
||||
| 'in_use' // Camera is in use by another application
|
||||
| 'permission_denied' // Camera permission was denied
|
||||
| 'not_found' // No camera found on device
|
||||
| 'error' // Generic error state
|
||||
| 'off'; // Camera is off/stopped
|
||||
export type CameraState =
|
||||
| "initializing" // Camera is being initialized
|
||||
| "ready" // Camera is ready to use
|
||||
| "active" // Camera is actively streaming
|
||||
| "in_use" // Camera is in use by another application
|
||||
| "permission_denied" // Camera permission was denied
|
||||
| "not_found" // No camera found on device
|
||||
| "error" // Generic error state
|
||||
| "off"; // Camera is off/stopped
|
||||
|
||||
export interface CameraStateListener {
|
||||
onStateChange: (state: CameraState, message?: string) => void;
|
||||
|
||||
@@ -80,7 +80,9 @@ export class WebPlatformService implements PlatformService {
|
||||
*/
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
const hasGetUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
|
||||
const hasGetUserMedia = !!(
|
||||
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
|
||||
);
|
||||
|
||||
// If on mobile, use file input with capture attribute (existing behavior)
|
||||
if (isMobile || !hasGetUserMedia) {
|
||||
@@ -113,107 +115,120 @@ export class WebPlatformService implements PlatformService {
|
||||
}
|
||||
|
||||
// Desktop: Use getUserMedia for webcam capture
|
||||
return new Promise(async (resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let stream: MediaStream | null = null;
|
||||
let video: HTMLVideoElement | null = null;
|
||||
let captureButton: HTMLButtonElement | null = null;
|
||||
let overlay: HTMLDivElement | null = null;
|
||||
let cleanup = () => {
|
||||
const cleanup = () => {
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
if (video && video.parentNode) video.parentNode.removeChild(video);
|
||||
if (captureButton && captureButton.parentNode) captureButton.parentNode.removeChild(captureButton);
|
||||
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
||||
if (captureButton && captureButton.parentNode)
|
||||
captureButton.parentNode.removeChild(captureButton);
|
||||
if (overlay && overlay.parentNode)
|
||||
overlay.parentNode.removeChild(overlay);
|
||||
};
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" } });
|
||||
// Create overlay for video and button
|
||||
overlay = document.createElement("div");
|
||||
overlay.style.position = "fixed";
|
||||
overlay.style.top = "0";
|
||||
overlay.style.left = "0";
|
||||
overlay.style.width = "100vw";
|
||||
overlay.style.height = "100vh";
|
||||
overlay.style.background = "rgba(0,0,0,0.8)";
|
||||
overlay.style.display = "flex";
|
||||
overlay.style.flexDirection = "column";
|
||||
overlay.style.justifyContent = "center";
|
||||
overlay.style.alignItems = "center";
|
||||
overlay.style.zIndex = "9999";
|
||||
|
||||
video = document.createElement("video");
|
||||
video.autoplay = true;
|
||||
video.playsInline = true;
|
||||
video.style.maxWidth = "90vw";
|
||||
video.style.maxHeight = "70vh";
|
||||
video.srcObject = stream;
|
||||
overlay.appendChild(video);
|
||||
// Move async operations inside Promise body
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "user" },
|
||||
})
|
||||
.then((mediaStream) => {
|
||||
stream = mediaStream;
|
||||
// Create overlay for video and button
|
||||
overlay = document.createElement("div");
|
||||
overlay.style.position = "fixed";
|
||||
overlay.style.top = "0";
|
||||
overlay.style.left = "0";
|
||||
overlay.style.width = "100vw";
|
||||
overlay.style.height = "100vh";
|
||||
overlay.style.background = "rgba(0,0,0,0.8)";
|
||||
overlay.style.display = "flex";
|
||||
overlay.style.flexDirection = "column";
|
||||
overlay.style.justifyContent = "center";
|
||||
overlay.style.alignItems = "center";
|
||||
overlay.style.zIndex = "9999";
|
||||
|
||||
captureButton = document.createElement("button");
|
||||
captureButton.textContent = "Capture Photo";
|
||||
captureButton.style.marginTop = "2rem";
|
||||
captureButton.style.padding = "1rem 2rem";
|
||||
captureButton.style.fontSize = "1.2rem";
|
||||
captureButton.style.background = "#2563eb";
|
||||
captureButton.style.color = "white";
|
||||
captureButton.style.border = "none";
|
||||
captureButton.style.borderRadius = "0.5rem";
|
||||
captureButton.style.cursor = "pointer";
|
||||
overlay.appendChild(captureButton);
|
||||
video = document.createElement("video");
|
||||
video.autoplay = true;
|
||||
video.playsInline = true;
|
||||
video.style.maxWidth = "90vw";
|
||||
video.style.maxHeight = "70vh";
|
||||
video.srcObject = stream;
|
||||
overlay.appendChild(video);
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
captureButton = document.createElement("button");
|
||||
captureButton.textContent = "Capture Photo";
|
||||
captureButton.style.marginTop = "2rem";
|
||||
captureButton.style.padding = "1rem 2rem";
|
||||
captureButton.style.fontSize = "1.2rem";
|
||||
captureButton.style.background = "#2563eb";
|
||||
captureButton.style.color = "white";
|
||||
captureButton.style.border = "none";
|
||||
captureButton.style.borderRadius = "0.5rem";
|
||||
captureButton.style.cursor = "pointer";
|
||||
overlay.appendChild(captureButton);
|
||||
|
||||
captureButton.onclick = async () => {
|
||||
try {
|
||||
// Create a canvas to capture the frame
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = video!.videoWidth;
|
||||
canvas.height = video!.videoHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.drawImage(video!, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob((blob) => {
|
||||
cleanup();
|
||||
if (blob) {
|
||||
resolve({
|
||||
blob,
|
||||
fileName: `photo_${Date.now()}.jpg`,
|
||||
});
|
||||
} else {
|
||||
reject(new Error("Failed to capture image from webcam"));
|
||||
}
|
||||
}, "image/jpeg", 0.95);
|
||||
} catch (err) {
|
||||
cleanup();
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
cleanup();
|
||||
logger.error("Error accessing webcam:", error);
|
||||
// Fallback to file input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
captureButton.onclick = () => {
|
||||
try {
|
||||
const blob = await this.processImageFile(file);
|
||||
resolve({
|
||||
blob,
|
||||
fileName: file.name || "photo.jpg",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error processing fallback image:", error);
|
||||
reject(new Error("Failed to process fallback image"));
|
||||
// Create a canvas to capture the frame
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = video!.videoWidth;
|
||||
canvas.height = video!.videoHeight;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.drawImage(video!, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob(
|
||||
(blob) => {
|
||||
cleanup();
|
||||
if (blob) {
|
||||
resolve({
|
||||
blob,
|
||||
fileName: `photo_${Date.now()}.jpg`,
|
||||
});
|
||||
} else {
|
||||
reject(new Error("Failed to capture image from webcam"));
|
||||
}
|
||||
},
|
||||
"image/jpeg",
|
||||
0.95,
|
||||
);
|
||||
} catch (err) {
|
||||
cleanup();
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
reject(new Error("No image selected"));
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
cleanup();
|
||||
logger.error("Error accessing webcam:", error);
|
||||
// Fallback to file input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
this.processImageFile(file)
|
||||
.then((blob) => {
|
||||
resolve({
|
||||
blob,
|
||||
fileName: file.name || "photo.jpg",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Error processing fallback image:", error);
|
||||
reject(new Error("Failed to process fallback image"));
|
||||
});
|
||||
} else {
|
||||
reject(new Error("No image selected"));
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
<TopMessage />
|
||||
|
||||
<!-- CONTENT -->
|
||||
<main id="Content" class="p-6 pb-24 max-w-3xl mx-auto" role="main" aria-label="Account Profile">
|
||||
<main
|
||||
id="Content"
|
||||
class="p-6 pb-24 max-w-3xl mx-auto"
|
||||
role="main"
|
||||
aria-label="Account Profile"
|
||||
>
|
||||
<!-- Heading -->
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
||||
Your Identity
|
||||
@@ -78,31 +83,28 @@
|
||||
:icon-size="96"
|
||||
:profile-image-url="profileImageUrl"
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||
@click="showLargeIdenticonUrl = profileImageUrl"
|
||||
role="button"
|
||||
aria-label="View profile image in large size"
|
||||
tabindex="0"
|
||||
@click="showLargeIdenticonUrl = profileImageUrl"
|
||||
/>
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
|
||||
@click="confirmDeleteImage"
|
||||
role="button"
|
||||
aria-label="Delete profile image"
|
||||
tabindex="0"
|
||||
@click="confirmDeleteImage"
|
||||
/>
|
||||
</span>
|
||||
<div v-else class="text-center">
|
||||
<template v-if="isRegistered">
|
||||
<div class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" @click="openImageDialog()">
|
||||
<font-awesome
|
||||
icon="user"
|
||||
class="fa-fw"
|
||||
/>
|
||||
<font-awesome
|
||||
icon="camera"
|
||||
class="fa-fw"
|
||||
/>
|
||||
<div
|
||||
class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="openImageDialog()"
|
||||
>
|
||||
<font-awesome icon="user" class="fa-fw" />
|
||||
<font-awesome icon="camera" class="fa-fw" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -124,7 +126,10 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<ImageMethodDialog ref="imageMethodDialog" :isRegistered="isRegistered" />
|
||||
<ImageMethodDialog
|
||||
ref="imageMethodDialog"
|
||||
:is-registered="isRegistered"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="flex justify-center text-center text-sm leading-tight mb-1">
|
||||
@@ -171,14 +176,20 @@
|
||||
<code class="truncate" aria-label="Your DID">{{ activeDid }}</code>
|
||||
<button
|
||||
class="ml-2"
|
||||
aria-label="Copy DID to clipboard"
|
||||
@click="
|
||||
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
|
||||
"
|
||||
aria-label="Copy DID to clipboard"
|
||||
>
|
||||
<font-awesome icon="copy" class="text-slate-400 fa-fw" aria-hidden="true"></font-awesome>
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
aria-hidden="true"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<span v-show="showDidCopy" role="status" aria-live="polite">Copied</span>
|
||||
<span v-show="showDidCopy" role="status" aria-live="polite"
|
||||
>Copied</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="text-blue-500 text-sm font-bold">
|
||||
@@ -201,8 +212,8 @@
|
||||
aria-live="polite"
|
||||
>
|
||||
<p class="mb-2">
|
||||
Before you can publicly announce a new project or time
|
||||
commitment, a friend needs to register you.
|
||||
Before you can publicly announce a new project or time commitment, a
|
||||
friend needs to register you.
|
||||
</p>
|
||||
<router-link
|
||||
:to="{ name: 'contact-qr' }"
|
||||
@@ -224,19 +235,22 @@
|
||||
Reminder Notification
|
||||
<button
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
@click.stop="showReminderNotificationInfo"
|
||||
aria-label="Learn more about reminder notifications"
|
||||
@click.stop="showReminderNotificationInfo"
|
||||
>
|
||||
<font-awesome icon="question-circle" aria-hidden="true"></font-awesome>
|
||||
<font-awesome
|
||||
icon="question-circle"
|
||||
aria-hidden="true"
|
||||
></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="relative ml-2 cursor-pointer"
|
||||
@click="showReminderNotificationChoice()"
|
||||
role="switch"
|
||||
:aria-checked="notifyingReminder"
|
||||
aria-label="Toggle reminder notifications"
|
||||
tabindex="0"
|
||||
@click="showReminderNotificationChoice()"
|
||||
>
|
||||
<!-- input -->
|
||||
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
|
||||
@@ -297,7 +311,9 @@
|
||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||
aria-labelledby="searchLocationHeading"
|
||||
>
|
||||
<h2 id="searchLocationHeading" class="mb-2 font-bold">Location for Searches</h2>
|
||||
<h2 id="searchLocationHeading" class="mb-2 font-bold">
|
||||
Location for Searches
|
||||
</h2>
|
||||
<router-link
|
||||
:to="{ name: 'search-area' }"
|
||||
class="block w-full text-center bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@@ -316,8 +332,8 @@
|
||||
Public Profile
|
||||
<button
|
||||
class="text-slate-400 fa-fw cursor-pointer"
|
||||
@click="showProfileInfo"
|
||||
aria-label="Learn more about public profile"
|
||||
@click="showProfileInfo"
|
||||
>
|
||||
<font-awesome icon="circle-info" aria-hidden="true"></font-awesome>
|
||||
</button>
|
||||
@@ -408,9 +424,18 @@
|
||||
>
|
||||
<h2 id="usageLimitsHeading" class="mb-2 font-bold">Usage Limits</h2>
|
||||
<!-- show spinner if loading limits -->
|
||||
<div v-if="loadingLimits" class="text-center" role="status" aria-live="polite">
|
||||
<div
|
||||
v-if="loadingLimits"
|
||||
class="text-center"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
Checking…
|
||||
<font-awesome icon="spinner" class="fa-spin" aria-hidden="true"></font-awesome>
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin"
|
||||
aria-hidden="true"
|
||||
></font-awesome>
|
||||
</div>
|
||||
<div class="mb-4 text-center">
|
||||
{{ limitsMessage }}
|
||||
@@ -468,9 +493,13 @@
|
||||
class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer"
|
||||
@click="showAdvanced = !showAdvanced"
|
||||
>
|
||||
{{ showAdvanced ? 'Hide Advanced Settings' : 'Show Advanced Settings' }}
|
||||
{{ showAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings" }}
|
||||
</h3>
|
||||
<section v-if="showAdvanced || showGeneralAdvanced" id="sectionAdvanced" aria-labelledby="advancedHeading">
|
||||
<section
|
||||
v-if="showAdvanced || showGeneralAdvanced"
|
||||
id="sectionAdvanced"
|
||||
aria-labelledby="advancedHeading"
|
||||
>
|
||||
<h2 id="advancedHeading" class="sr-only">Advanced Settings</h2>
|
||||
<p class="text-rose-600 mb-8">
|
||||
Beware: the features here can be confusing and even change data in ways
|
||||
@@ -642,8 +671,14 @@
|
||||
|
||||
<div id="sectionClaimServer">
|
||||
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
|
||||
<div class="px-4 py-4" role="group" aria-labelledby="claimServerHeading">
|
||||
<h3 id="claimServerHeading" class="sr-only">Claim Server Configuration</h3>
|
||||
<div
|
||||
class="px-4 py-4"
|
||||
role="group"
|
||||
aria-labelledby="claimServerHeading"
|
||||
>
|
||||
<h3 id="claimServerHeading" class="sr-only">
|
||||
Claim Server Configuration
|
||||
</h3>
|
||||
<label for="apiServerInput" class="sr-only">API Server URL</label>
|
||||
<input
|
||||
id="apiServerInput"
|
||||
@@ -653,18 +688,15 @@
|
||||
aria-describedby="apiServerDescription"
|
||||
placeholder="Enter API server URL"
|
||||
/>
|
||||
<div
|
||||
id="apiServerDescription"
|
||||
class="sr-only"
|
||||
role="tooltip"
|
||||
>
|
||||
Enter the URL for the claim server. You can use the buttons below to quickly set common server URLs.
|
||||
<div id="apiServerDescription" class="sr-only" role="tooltip">
|
||||
Enter the URL for the claim server. You can use the buttons below to
|
||||
quickly set common server URLs.
|
||||
</div>
|
||||
<button
|
||||
v-if="apiServerInput != apiServer"
|
||||
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||
@click="onClickSaveApiServer()"
|
||||
aria-label="Save API server URL"
|
||||
@click="onClickSaveApiServer()"
|
||||
>
|
||||
<font-awesome
|
||||
icon="floppy-disk"
|
||||
@@ -676,22 +708,22 @@
|
||||
<div class="mt-2" role="group" aria-label="Quick server selection">
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
||||
aria-label="Use production server URL"
|
||||
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
|
||||
>
|
||||
Use Prod
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
||||
aria-label="Use test server URL"
|
||||
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
|
||||
>
|
||||
Use Test
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
||||
aria-label="Use local server URL"
|
||||
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
|
||||
>
|
||||
Use Local
|
||||
</button>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 my-4"
|
||||
>
|
||||
<p class="mb-2">
|
||||
<b>Note:</b> your identity currently does <b>not</b> include a name.
|
||||
<b>Note:</b> your identity currently does <b>not</b> include a name.
|
||||
</p>
|
||||
<button
|
||||
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@@ -112,7 +112,7 @@
|
||||
3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{{ cameraStateMessage || 'Initializing camera...' }}</span>
|
||||
<span>{{ cameraStateMessage || "Initializing camera..." }}</span>
|
||||
</div>
|
||||
<p
|
||||
v-else-if="cameraState === 'active'"
|
||||
@@ -125,14 +125,19 @@
|
||||
</p>
|
||||
<p v-else-if="error" class="text-red-400">Error: {{ error }}</p>
|
||||
<p v-else class="flex items-center justify-center space-x-2">
|
||||
<span :class="{
|
||||
'inline-block w-2 h-2 rounded-full': true,
|
||||
'bg-green-500': cameraState === 'ready',
|
||||
'bg-yellow-500': cameraState === 'in_use',
|
||||
'bg-red-500': cameraState === 'error' || cameraState === 'permission_denied' || cameraState === 'not_found',
|
||||
'bg-blue-500': cameraState === 'off'
|
||||
}"></span>
|
||||
<span>{{ cameraStateMessage || 'Ready to scan' }}</span>
|
||||
<span
|
||||
:class="{
|
||||
'inline-block w-2 h-2 rounded-full': true,
|
||||
'bg-green-500': cameraState === 'ready',
|
||||
'bg-yellow-500': cameraState === 'in_use',
|
||||
'bg-red-500':
|
||||
cameraState === 'error' ||
|
||||
cameraState === 'permission_denied' ||
|
||||
cameraState === 'not_found',
|
||||
'bg-blue-500': cameraState === 'off',
|
||||
}"
|
||||
></span>
|
||||
<span>{{ cameraStateMessage || "Ready to scan" }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -246,7 +251,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
initializationStatus = "Initializing camera...";
|
||||
useQRReader = __USE_QR_READER__;
|
||||
preferredCamera: "user" | "environment" = "environment";
|
||||
cameraState: CameraState = 'off';
|
||||
cameraState: CameraState = "off";
|
||||
cameraStateMessage?: string;
|
||||
|
||||
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
||||
@@ -321,36 +326,36 @@ export default class ContactQRScanShow extends Vue {
|
||||
onStateChange: (state, message) => {
|
||||
this.cameraState = state;
|
||||
this.cameraStateMessage = message;
|
||||
|
||||
|
||||
// Update UI based on camera state
|
||||
switch (state) {
|
||||
case 'in_use':
|
||||
case "in_use":
|
||||
this.error = "Camera is in use by another application";
|
||||
this.isScanning = false;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
this.isScanning = false;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Camera in Use",
|
||||
text: "Please close other applications using the camera and try again",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
},
|
||||
5000,
|
||||
);
|
||||
break;
|
||||
case 'permission_denied':
|
||||
this.error = "Camera permission denied";
|
||||
this.isScanning = false;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Camera Access Required",
|
||||
case "permission_denied":
|
||||
this.error = "Camera permission denied";
|
||||
this.isScanning = false;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Camera Access Required",
|
||||
text: "Please grant camera permission to scan QR codes",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
},
|
||||
5000,
|
||||
);
|
||||
break;
|
||||
case 'not_found':
|
||||
case "not_found":
|
||||
this.error = "No camera found";
|
||||
this.isScanning = false;
|
||||
this.$notify(
|
||||
@@ -363,7 +368,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
5000,
|
||||
);
|
||||
break;
|
||||
case 'error':
|
||||
case "error":
|
||||
this.error = this.cameraStateMessage || "Camera error";
|
||||
this.isScanning = false;
|
||||
break;
|
||||
@@ -373,7 +378,8 @@ export default class ContactQRScanShow extends Vue {
|
||||
|
||||
// Check if scanning is supported first
|
||||
if (!(await scanner.isSupported())) {
|
||||
this.error = "Camera access requires HTTPS. Please use a secure connection.";
|
||||
this.error =
|
||||
"Camera access requires HTTPS. Please use a secure connection.";
|
||||
this.isScanning = false;
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user