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. 47
      src/components/PhotoDialog.vue
  3. 107
      src/services/QRScanner/WebInlineQRScanner.ts
  4. 16
      src/services/QRScanner/types.ts
  5. 187
      src/services/platforms/WebPlatformService.ts
  6. 110
      src/views/AccountViewView.vue
  7. 74
      src/views/ContactQRScanShowView.vue

4
src/components/ImageMethodDialog.vue

@ -51,7 +51,9 @@
</div> </div>
</template> </template>
<template v-else> <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> </template>
</div> </div>
</div> </div>

47
src/components/PhotoDialog.vue

@ -173,12 +173,12 @@ export default class PhotoDialog extends Vue {
* @throws {Error} When settings retrieval fails * @throws {Error} When settings retrieval fails
*/ */
async mounted() { async mounted() {
console.log('PhotoDialog mounted'); logger.log("PhotoDialog mounted");
try { try {
const settings = await retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered; this.isRegistered = !!settings.isRegistered;
console.log('isRegistered:', this.isRegistered); logger.log("isRegistered:", this.isRegistered);
} catch (error: unknown) { } catch (error: unknown) {
logger.error("Error retrieving settings from database:", error); logger.error("Error retrieving settings from database:", error);
this.$notify( this.$notify(
@ -245,7 +245,10 @@ export default class PhotoDialog extends Vue {
* Closes the photo dialog and resets state * Closes the photo dialog and resets state
*/ */
close() { close() {
logger.debug("Dialog closing, current showCameraPreview:", this.showCameraPreview); logger.debug(
"Dialog closing, current showCameraPreview:",
this.showCameraPreview,
);
this.visible = false; this.visible = false;
this.stopCameraPreview(); this.stopCameraPreview();
const bottomNav = document.querySelector("#QuickNav") as HTMLElement; const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
@ -294,7 +297,10 @@ export default class PhotoDialog extends Vue {
// Force a re-render // Force a re-render
await this.$nextTick(); await this.$nextTick();
logger.debug("After nextTick, showCameraPreview is:", this.showCameraPreview); logger.debug(
"After nextTick, showCameraPreview is:",
this.showCameraPreview,
);
logger.debug("Requesting camera access..."); logger.debug("Requesting camera access...");
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
@ -305,7 +311,10 @@ export default class PhotoDialog extends Vue {
// Force another re-render after getting the stream // Force another re-render after getting the stream
await this.$nextTick(); 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; const videoElement = this.$refs.videoElement as HTMLVideoElement;
if (videoElement) { if (videoElement) {
@ -343,13 +352,19 @@ export default class PhotoDialog extends Vue {
* Stops the camera preview and cleans up resources * Stops the camera preview and cleans up resources
*/ */
stopCameraPreview() { stopCameraPreview() {
logger.debug("Stopping camera preview, current showCameraPreview:", this.showCameraPreview); logger.debug(
"Stopping camera preview, current showCameraPreview:",
this.showCameraPreview,
);
if (this.cameraStream) { if (this.cameraStream) {
this.cameraStream.getTracks().forEach((track) => track.stop()); this.cameraStream.getTracks().forEach((track) => track.stop());
this.cameraStream = null; this.cameraStream = null;
} }
this.showCameraPreview = false; 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"); const ctx = canvas.getContext("2d");
ctx?.drawImage(videoElement, 0, 0, canvas.width, canvas.height); ctx?.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
canvas.toBlob((blob) => { canvas.toBlob(
if (blob) { (blob) => {
this.blob = blob; if (blob) {
this.fileName = `photo_${Date.now()}.jpg`; this.blob = blob;
this.stopCameraPreview(); this.fileName = `photo_${Date.now()}.jpg`;
} this.stopCameraPreview();
}, "image/jpeg", 0.95); }
},
"image/jpeg",
0.95,
);
} catch (error) { } catch (error) {
logger.error("Error capturing photo:", error); logger.error("Error capturing photo:", error);
this.$notify( this.$notify(

107
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 { logger } from "@/utils/logger";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import jsQR from "jsqr"; import jsQR from "jsqr";
@ -22,7 +28,7 @@ export class WebInlineQRScanner implements QRScannerService {
private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames
private lastFrameTime = 0; private lastFrameTime = 0;
private cameraStateListeners: Set<CameraStateListener> = new Set(); private cameraStateListeners: Set<CameraStateListener> = new Set();
private currentState: CameraState = 'off'; private currentState: CameraState = "off";
private currentStateMessage?: string; private currentStateMessage?: string;
constructor(private options?: QRScannerOptions) { constructor(private options?: QRScannerOptions) {
@ -49,15 +55,21 @@ export class WebInlineQRScanner implements QRScannerService {
private updateCameraState(state: CameraState, message?: string) { private updateCameraState(state: CameraState, message?: string) {
this.currentState = state; this.currentState = state;
this.currentStateMessage = message; this.currentStateMessage = message;
this.cameraStateListeners.forEach(listener => { this.cameraStateListeners.forEach((listener) => {
try { try {
listener.onStateChange(state, message); listener.onStateChange(state, message);
logger.info(`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`, { logger.info(
state, `[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
message, {
}); state,
message,
},
);
} catch (error) { } 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> { async checkPermissions(): Promise<boolean> {
try { try {
this.updateCameraState('initializing', 'Checking camera permissions...'); this.updateCameraState("initializing", "Checking camera permissions...");
logger.error( logger.error(
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`, `[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
); );
@ -86,7 +98,7 @@ export class WebInlineQRScanner implements QRScannerService {
permissions.state, permissions.state,
); );
const granted = permissions.state === "granted"; const granted = permissions.state === "granted";
this.updateCameraState(granted ? 'ready' : 'permission_denied'); this.updateCameraState(granted ? "ready" : "permission_denied");
return granted; return granted;
} catch (error) { } catch (error) {
logger.error( logger.error(
@ -96,14 +108,17 @@ export class WebInlineQRScanner implements QRScannerService {
stack: error instanceof Error ? error.stack : undefined, stack: error instanceof Error ? error.stack : undefined,
}, },
); );
this.updateCameraState('error', 'Error checking camera permissions'); this.updateCameraState("error", "Error checking camera permissions");
return false; return false;
} }
} }
async requestPermissions(): Promise<boolean> { async requestPermissions(): Promise<boolean> {
try { try {
this.updateCameraState('initializing', 'Requesting camera permissions...'); this.updateCameraState(
"initializing",
"Requesting camera permissions...",
);
logger.error( logger.error(
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`, `[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
); );
@ -141,7 +156,7 @@ export class WebInlineQRScanner implements QRScannerService {
}, },
}); });
this.updateCameraState('ready', 'Camera permissions granted'); this.updateCameraState("ready", "Camera permissions granted");
// Stop the test stream immediately // Stop the test stream immediately
stream.getTracks().forEach((track) => { stream.getTracks().forEach((track) => {
@ -154,20 +169,35 @@ export class WebInlineQRScanner implements QRScannerService {
}); });
return true; return true;
} catch (error) { } 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 // Update state based on error type
if (wrappedError.name === "NotFoundError" || wrappedError.name === "DevicesNotFoundError") { if (
this.updateCameraState('not_found', 'No camera found on this device'); 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"); throw new Error("No camera found on this device");
} else if (wrappedError.name === "NotAllowedError" || wrappedError.name === "PermissionDeniedError") { } else if (
this.updateCameraState('permission_denied', 'Camera access denied'); wrappedError.name === "NotAllowedError" ||
throw new Error("Camera access denied. Please grant camera permission and try again"); wrappedError.name === "PermissionDeniedError"
} else if (wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError") { ) {
this.updateCameraState('in_use', 'Camera is in use by another application'); 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"); throw new Error("Camera is in use by another application");
} else { } else {
this.updateCameraState('error', wrappedError.message); this.updateCameraState("error", wrappedError.message);
throw new Error(`Camera error: ${wrappedError.message}`); throw new Error(`Camera error: ${wrappedError.message}`);
} }
} }
@ -406,7 +436,7 @@ export class WebInlineQRScanner implements QRScannerService {
this.isScanning = true; this.isScanning = true;
this.scanAttempts = 0; this.scanAttempts = 0;
this.lastScanTime = Date.now(); this.lastScanTime = Date.now();
this.updateCameraState('initializing', 'Starting camera...'); this.updateCameraState("initializing", "Starting camera...");
logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`); logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`);
// Get camera stream // Get camera stream
@ -421,7 +451,7 @@ 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:`, { logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
tracks: this.stream.getTracks().map((t) => ({ tracks: this.stream.getTracks().map((t) => ({
@ -448,13 +478,20 @@ export class WebInlineQRScanner implements QRScannerService {
this.scanQRCode(); this.scanQRCode();
} catch (error) { } catch (error) {
this.isScanning = false; 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 // Update state based on error type
if (wrappedError.name === "NotReadableError" || wrappedError.name === "TrackStartError") { if (
this.updateCameraState('in_use', 'Camera is in use by another application'); wrappedError.name === "NotReadableError" ||
wrappedError.name === "TrackStartError"
) {
this.updateCameraState(
"in_use",
"Camera is in use by another application",
);
} else { } else {
this.updateCameraState('error', wrappedError.message); this.updateCameraState("error", wrappedError.message);
} }
if (this.scanListener?.onError) { if (this.scanListener?.onError) {
@ -513,8 +550,11 @@ export class WebInlineQRScanner implements QRScannerService {
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`, `[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
); );
} catch (error) { } catch (error) {
logger.error(`[WebInlineQRScanner:${this.id}] Error stopping scan:`, error); logger.error(
this.updateCameraState('error', 'Error stopping camera'); `[WebInlineQRScanner:${this.id}] Error stopping scan:`,
error,
);
this.updateCameraState("error", "Error stopping camera");
throw error; throw error;
} finally { } finally {
this.isScanning = false; this.isScanning = false;
@ -557,8 +597,11 @@ export class WebInlineQRScanner implements QRScannerService {
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`, `[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
); );
} catch (error) { } catch (error) {
logger.error(`[WebInlineQRScanner:${this.id}] Error during cleanup:`, error); logger.error(
this.updateCameraState('error', 'Error during cleanup'); `[WebInlineQRScanner:${this.id}] Error during cleanup:`,
error,
);
this.updateCameraState("error", "Error during cleanup");
throw error; throw error;
} }
} }

16
src/services/QRScanner/types.ts

@ -23,14 +23,14 @@ export interface QRScannerOptions {
} }
export type CameraState = export type CameraState =
| 'initializing' // Camera is being initialized | "initializing" // Camera is being initialized
| 'ready' // Camera is ready to use | "ready" // Camera is ready to use
| 'active' // Camera is actively streaming | "active" // Camera is actively streaming
| 'in_use' // Camera is in use by another application | "in_use" // Camera is in use by another application
| 'permission_denied' // Camera permission was denied | "permission_denied" // Camera permission was denied
| 'not_found' // No camera found on device | "not_found" // No camera found on device
| 'error' // Generic error state | "error" // Generic error state
| 'off'; // Camera is off/stopped | "off"; // Camera is off/stopped
export interface CameraStateListener { export interface CameraStateListener {
onStateChange: (state: CameraState, message?: string) => void; 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> { async takePicture(): Promise<ImageResult> {
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); 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 on mobile, use file input with capture attribute (existing behavior)
if (isMobile || !hasGetUserMedia) { if (isMobile || !hasGetUserMedia) {
@ -113,107 +115,120 @@ export class WebPlatformService implements PlatformService {
} }
// Desktop: Use getUserMedia for webcam capture // Desktop: Use getUserMedia for webcam capture
return new Promise(async (resolve, reject) => { return new Promise((resolve, reject) => {
let stream: MediaStream | null = null; let stream: MediaStream | null = null;
let video: HTMLVideoElement | null = null; let video: HTMLVideoElement | null = null;
let captureButton: HTMLButtonElement | null = null; let captureButton: HTMLButtonElement | null = null;
let overlay: HTMLDivElement | null = null; let overlay: HTMLDivElement | null = null;
let cleanup = () => { const cleanup = () => {
if (stream) { if (stream) {
stream.getTracks().forEach((track) => track.stop()); stream.getTracks().forEach((track) => track.stop());
} }
if (video && video.parentNode) video.parentNode.removeChild(video); if (video && video.parentNode) video.parentNode.removeChild(video);
if (captureButton && captureButton.parentNode) captureButton.parentNode.removeChild(captureButton); if (captureButton && captureButton.parentNode)
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); 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"); // Move async operations inside Promise body
video.autoplay = true; navigator.mediaDevices.getUserMedia({
video.playsInline = true; video: { facingMode: "user" },
video.style.maxWidth = "90vw"; })
video.style.maxHeight = "70vh"; .then((mediaStream) => {
video.srcObject = stream; stream = mediaStream;
overlay.appendChild(video); // 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"); video = document.createElement("video");
captureButton.textContent = "Capture Photo"; video.autoplay = true;
captureButton.style.marginTop = "2rem"; video.playsInline = true;
captureButton.style.padding = "1rem 2rem"; video.style.maxWidth = "90vw";
captureButton.style.fontSize = "1.2rem"; video.style.maxHeight = "70vh";
captureButton.style.background = "#2563eb"; video.srcObject = stream;
captureButton.style.color = "white"; overlay.appendChild(video);
captureButton.style.border = "none";
captureButton.style.borderRadius = "0.5rem";
captureButton.style.cursor = "pointer";
overlay.appendChild(captureButton);
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 () => { document.body.appendChild(overlay);
try {
// Create a canvas to capture the frame captureButton.onclick = () => {
const canvas = document.createElement("canvas"); try {
canvas.width = video!.videoWidth; // Create a canvas to capture the frame
canvas.height = video!.videoHeight; const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d"); canvas.width = video!.videoWidth;
ctx?.drawImage(video!, 0, 0, canvas.width, canvas.height); canvas.height = video!.videoHeight;
canvas.toBlob((blob) => { 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(); cleanup();
if (blob) { reject(err);
resolve({ }
blob, };
fileName: `photo_${Date.now()}.jpg`, })
.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 { } else {
reject(new Error("Failed to capture image from webcam")); reject(new Error("No image selected"));
}
}, "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")); input.click();
} });
};
input.click();
}
}); });
} }

110
src/views/AccountViewView.vue

@ -3,7 +3,12 @@
<TopMessage /> <TopMessage />
<!-- CONTENT --> <!-- 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 --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light"> <h1 id="ViewHeading" class="text-4xl text-center font-light">
Your Identity Your Identity
@ -78,31 +83,28 @@
:icon-size="96" :icon-size="96"
:profile-image-url="profileImageUrl" :profile-image-url="profileImageUrl"
class="inline-block align-text-bottom border border-slate-300 rounded" class="inline-block align-text-bottom border border-slate-300 rounded"
@click="showLargeIdenticonUrl = profileImageUrl"
role="button" role="button"
aria-label="View profile image in large size" aria-label="View profile image in large size"
tabindex="0" tabindex="0"
@click="showLargeIdenticonUrl = profileImageUrl"
/> />
<font-awesome <font-awesome
icon="trash-can" icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12" class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
@click="confirmDeleteImage"
role="button" role="button"
aria-label="Delete profile image" aria-label="Delete profile image"
tabindex="0" tabindex="0"
@click="confirmDeleteImage"
/> />
</span> </span>
<div v-else class="text-center"> <div v-else class="text-center">
<template v-if="isRegistered"> <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()"> <div
<font-awesome 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"
icon="user" @click="openImageDialog()"
class="fa-fw" >
/> <font-awesome icon="user" class="fa-fw" />
<font-awesome <font-awesome icon="camera" class="fa-fw" />
icon="camera"
class="fa-fw"
/>
</div> </div>
</template> </template>
<template v-else> <template v-else>
@ -124,7 +126,10 @@
</div> </div>
</template> </template>
</div> </div>
<ImageMethodDialog ref="imageMethodDialog" :isRegistered="isRegistered" /> <ImageMethodDialog
ref="imageMethodDialog"
:is-registered="isRegistered"
/>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<div class="flex justify-center text-center text-sm leading-tight mb-1"> <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> <code class="truncate" aria-label="Your DID">{{ activeDid }}</code>
<button <button
class="ml-2" class="ml-2"
aria-label="Copy DID to clipboard"
@click=" @click="
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy)) 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> </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>
<div class="text-blue-500 text-sm font-bold"> <div class="text-blue-500 text-sm font-bold">
@ -201,8 +212,8 @@
aria-live="polite" aria-live="polite"
> >
<p class="mb-2"> <p class="mb-2">
Before you can publicly announce a new project or time Before you can publicly announce a new project or time commitment, a
commitment, a friend needs to register you. friend needs to register you.
</p> </p>
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
@ -224,19 +235,22 @@
Reminder Notification Reminder Notification
<button <button
class="text-slate-400 fa-fw cursor-pointer" class="text-slate-400 fa-fw cursor-pointer"
@click.stop="showReminderNotificationInfo"
aria-label="Learn more about reminder notifications" 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> </button>
</div> </div>
<div <div
class="relative ml-2 cursor-pointer" class="relative ml-2 cursor-pointer"
@click="showReminderNotificationChoice()"
role="switch" role="switch"
:aria-checked="notifyingReminder" :aria-checked="notifyingReminder"
aria-label="Toggle reminder notifications" aria-label="Toggle reminder notifications"
tabindex="0" tabindex="0"
@click="showReminderNotificationChoice()"
> >
<!-- input --> <!-- input -->
<input v-model="notifyingReminder" type="checkbox" class="sr-only" /> <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" class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
aria-labelledby="searchLocationHeading" 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 <router-link
:to="{ name: 'search-area' }" :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" 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 Public Profile
<button <button
class="text-slate-400 fa-fw cursor-pointer" class="text-slate-400 fa-fw cursor-pointer"
@click="showProfileInfo"
aria-label="Learn more about public profile" aria-label="Learn more about public profile"
@click="showProfileInfo"
> >
<font-awesome icon="circle-info" aria-hidden="true"></font-awesome> <font-awesome icon="circle-info" aria-hidden="true"></font-awesome>
</button> </button>
@ -408,9 +424,18 @@
> >
<h2 id="usageLimitsHeading" class="mb-2 font-bold">Usage Limits</h2> <h2 id="usageLimitsHeading" class="mb-2 font-bold">Usage Limits</h2>
<!-- show spinner if loading limits --> <!-- 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; 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>
<div class="mb-4 text-center"> <div class="mb-4 text-center">
{{ limitsMessage }} {{ limitsMessage }}
@ -468,9 +493,13 @@
class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer" class="text-blue-500 text-sm font-semibold mb-3 cursor-pointer"
@click="showAdvanced = !showAdvanced" @click="showAdvanced = !showAdvanced"
> >
{{ showAdvanced ? 'Hide Advanced Settings' : 'Show Advanced Settings' }} {{ showAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings" }}
</h3> </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> <h2 id="advancedHeading" class="sr-only">Advanced Settings</h2>
<p class="text-rose-600 mb-8"> <p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways Beware: the features here can be confusing and even change data in ways
@ -642,8 +671,14 @@
<div id="sectionClaimServer"> <div id="sectionClaimServer">
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2> <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"> <div
<h3 id="claimServerHeading" class="sr-only">Claim Server Configuration</h3> 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> <label for="apiServerInput" class="sr-only">API Server URL</label>
<input <input
id="apiServerInput" id="apiServerInput"
@ -653,18 +688,15 @@
aria-describedby="apiServerDescription" aria-describedby="apiServerDescription"
placeholder="Enter API server URL" placeholder="Enter API server URL"
/> />
<div <div id="apiServerDescription" class="sr-only" role="tooltip">
id="apiServerDescription" Enter the URL for the claim server. You can use the buttons below to
class="sr-only" quickly set common server URLs.
role="tooltip"
>
Enter the URL for the claim server. You can use the buttons below to quickly set common server URLs.
</div> </div>
<button <button
v-if="apiServerInput != apiServer" v-if="apiServerInput != apiServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400" class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSaveApiServer()"
aria-label="Save API server URL" aria-label="Save API server URL"
@click="onClickSaveApiServer()"
> >
<font-awesome <font-awesome
icon="floppy-disk" icon="floppy-disk"
@ -676,22 +708,22 @@
<div class="mt-2" role="group" aria-label="Quick server selection"> <div class="mt-2" role="group" aria-label="Quick server selection">
<button <button
class="px-3 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
aria-label="Use production server URL" aria-label="Use production server URL"
@click="apiServerInput = AppConstants.PROD_ENDORSER_API_SERVER"
> >
Use Prod Use Prod
</button> </button>
<button <button
class="px-3 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
aria-label="Use test server URL" aria-label="Use test server URL"
@click="apiServerInput = AppConstants.TEST_ENDORSER_API_SERVER"
> >
Use Test Use Test
</button> </button>
<button <button
class="px-3 rounded bg-slate-200 border border-slate-400" class="px-3 rounded bg-slate-200 border border-slate-400"
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
aria-label="Use local server URL" aria-label="Use local server URL"
@click="apiServerInput = AppConstants.LOCAL_ENDORSER_API_SERVER"
> >
Use Local Use Local
</button> </button>

74
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" 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"> <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> </p>
<button <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" 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" 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> ></path>
</svg> </svg>
<span>{{ cameraStateMessage || 'Initializing camera...' }}</span> <span>{{ cameraStateMessage || "Initializing camera..." }}</span>
</div> </div>
<p <p
v-else-if="cameraState === 'active'" v-else-if="cameraState === 'active'"
@ -125,14 +125,19 @@
</p> </p>
<p v-else-if="error" class="text-red-400">Error: {{ error }}</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"> <p v-else class="flex items-center justify-center space-x-2">
<span :class="{ <span
'inline-block w-2 h-2 rounded-full': true, :class="{
'bg-green-500': cameraState === 'ready', 'inline-block w-2 h-2 rounded-full': true,
'bg-yellow-500': cameraState === 'in_use', 'bg-green-500': cameraState === 'ready',
'bg-red-500': cameraState === 'error' || cameraState === 'permission_denied' || cameraState === 'not_found', 'bg-yellow-500': cameraState === 'in_use',
'bg-blue-500': cameraState === 'off' 'bg-red-500':
}"></span> cameraState === 'error' ||
<span>{{ cameraStateMessage || 'Ready to scan' }}</span> cameraState === 'permission_denied' ||
cameraState === 'not_found',
'bg-blue-500': cameraState === 'off',
}"
></span>
<span>{{ cameraStateMessage || "Ready to scan" }}</span>
</p> </p>
</div> </div>
@ -246,7 +251,7 @@ export default class ContactQRScanShow extends Vue {
initializationStatus = "Initializing camera..."; initializationStatus = "Initializing camera...";
useQRReader = __USE_QR_READER__; useQRReader = __USE_QR_READER__;
preferredCamera: "user" | "environment" = "environment"; preferredCamera: "user" | "environment" = "environment";
cameraState: CameraState = 'off'; cameraState: CameraState = "off";
cameraStateMessage?: string; cameraStateMessage?: string;
ETHR_DID_PREFIX = ETHR_DID_PREFIX; ETHR_DID_PREFIX = ETHR_DID_PREFIX;
@ -324,33 +329,33 @@ export default class ContactQRScanShow extends Vue {
// Update UI based on camera state // Update UI based on camera state
switch (state) { switch (state) {
case 'in_use': case "in_use":
this.error = "Camera is in use by another application"; this.error = "Camera is in use by another application";
this.isScanning = false; this.isScanning = false;
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "warning", type: "warning",
title: "Camera in Use", title: "Camera in Use",
text: "Please close other applications using the camera and try again", text: "Please close other applications using the camera and try again",
}, },
5000, 5000,
); );
break; break;
case 'permission_denied': case "permission_denied":
this.error = "Camera permission denied"; this.error = "Camera permission denied";
this.isScanning = false; this.isScanning = false;
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "warning", type: "warning",
title: "Camera Access Required", title: "Camera Access Required",
text: "Please grant camera permission to scan QR codes", text: "Please grant camera permission to scan QR codes",
}, },
5000, 5000,
); );
break; break;
case 'not_found': case "not_found":
this.error = "No camera found"; this.error = "No camera found";
this.isScanning = false; this.isScanning = false;
this.$notify( this.$notify(
@ -363,7 +368,7 @@ export default class ContactQRScanShow extends Vue {
5000, 5000,
); );
break; break;
case 'error': case "error":
this.error = this.cameraStateMessage || "Camera error"; this.error = this.cameraStateMessage || "Camera error";
this.isScanning = false; this.isScanning = false;
break; break;
@ -373,7 +378,8 @@ export default class ContactQRScanShow extends Vue {
// Check if scanning is supported first // Check if scanning is supported first
if (!(await scanner.isSupported())) { 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.isScanning = false;
this.$notify( this.$notify(
{ {

Loading…
Cancel
Save