Browse Source

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.
qrcode-reboot
Matt Raymer 2 days ago
parent
commit
a86e577127
  1. 4
      src/components/ImageMethodDialog.vue
  2. 51
      src/components/PhotoDialog.vue
  3. 117
      src/services/QRScanner/WebInlineQRScanner.ts
  4. 18
      src/services/QRScanner/types.ts
  5. 187
      src/services/platforms/WebPlatformService.ts
  6. 110
      src/views/AccountViewView.vue
  7. 76
      src/views/ContactQRScanShowView.vue

4
src/components/ImageMethodDialog.vue

@ -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>

51
src/components/PhotoDialog.vue

@ -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(

117
src/services/QRScanner/WebInlineQRScanner.ts

@ -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;
}
}

18
src/services/QRScanner/types.ts

@ -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;

187
src/services/platforms/WebPlatformService.ts

@ -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) => {
document.body.appendChild(overlay);
captureButton.onclick = () => {
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();
if (blob) {
resolve({
blob,
fileName: `photo_${Date.now()}.jpg`,
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 = (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("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) {
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"));
} else {
reject(new Error("No image selected"));
}
} else {
reject(new Error("No image selected"));
}
};
input.click();
}
};
input.click();
});
});
}

110
src/views/AccountViewView.vue

@ -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&hellip;
<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>

76
src/views/ContactQRScanShowView.vue

@ -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(
{

Loading…
Cancel
Save