You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							950 lines
						
					
					
						
							30 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							950 lines
						
					
					
						
							30 KiB
						
					
					
				
								<template>
							 | 
						|
								  <div v-if="visible" class="dialog-overlay">
							 | 
						|
								    <div class="dialog relative">
							 | 
						|
								      <div class="text-lg text-center font-bold relative">
							 | 
						|
								        <h1 id="ViewHeading" class="text-center font-bold">
							 | 
						|
								          <span v-if="uploading">Uploading Image…</span>
							 | 
						|
								          <span v-else-if="blob">{{
							 | 
						|
								            crop ? "Crop Image" : "Preview Image"
							 | 
						|
								          }}</span>
							 | 
						|
								          <span v-else-if="showCameraPreview">Upload Image</span>
							 | 
						|
								          <span v-else>Add Photo</span>
							 | 
						|
								        </h1>
							 | 
						|
								        <div
							 | 
						|
								          class="text-2xl text-center px-1 py-0.5 leading-none absolute -right-1 top-0"
							 | 
						|
								          @click="close()"
							 | 
						|
								        >
							 | 
						|
								          <font-awesome icon="xmark" class="w-[1em]"></font-awesome>
							 | 
						|
								        </div>
							 | 
						|
								      </div>
							 | 
						|
								
							 | 
						|
								      <!-- FEEDBACK: Show if camera preview is not visible after mounting -->
							 | 
						|
								      <div
							 | 
						|
								        v-if="!showCameraPreview && !blob && isRegistered && !isRetrying"
							 | 
						|
								        class="bg-red-100 text-red-700 border border-red-400 rounded px-4 py-3 my-4 text-sm"
							 | 
						|
								      >
							 | 
						|
								        <strong>Camera preview not started.</strong>
							 | 
						|
								        <div v-if="cameraState === 'off'">
							 | 
						|
								          <span v-if="platformCapabilities.isMobile">
							 | 
						|
								            <b>Note:</b> This mobile browser may not support direct camera
							 | 
						|
								            access, or the app is treating it as a native app.<br />
							 | 
						|
								            <b>Tip:</b> Try using a desktop browser, or check if your browser
							 | 
						|
								            supports camera access for web apps.<br />
							 | 
						|
								            <b>Developer:</b> The platform detection logic may be skipping
							 | 
						|
								            camera preview for mobile browsers. <br />
							 | 
						|
								            <b>Action:</b> Review <code>platformCapabilities.isMobile</code> and
							 | 
						|
								            ensure web browsers on mobile are not treated as native apps.
							 | 
						|
								          </span>
							 | 
						|
								          <span v-else>
							 | 
						|
								            <b>Tip:</b> Your browser supports camera APIs, but the preview did
							 | 
						|
								            not start. Try refreshing the page or checking browser permissions.
							 | 
						|
								          </span>
							 | 
						|
								        </div>
							 | 
						|
								        <div v-else-if="cameraState === 'error'">
							 | 
						|
								          <b>Error:</b> {{ error || cameraStateMessage }}
							 | 
						|
								        </div>
							 | 
						|
								        <div v-else>
							 | 
						|
								          <b>Status:</b> {{ cameraStateMessage || "Unknown reason." }}
							 | 
						|
								        </div>
							 | 
						|
								      </div>
							 | 
						|
								
							 | 
						|
								      <div class="mt-4">
							 | 
						|
								        <template v-if="isRegistered">
							 | 
						|
								          <div v-if="!blob">
							 | 
						|
								            <div :class="sectionDividerClasses">
							 | 
						|
								              <span :class="sectionDividerSpanClasses">
							 | 
						|
								                Take a photo with your camera
							 | 
						|
								              </span>
							 | 
						|
								            </div>
							 | 
						|
								            <div v-if="showCameraPreview" :class="cameraPreviewClasses">
							 | 
						|
								              <!-- Diagnostic Panel -->
							 | 
						|
								              <div v-if="showDiagnostics" :class="diagnosticsPanelClasses">
							 | 
						|
								                <div class="grid grid-cols-2 gap-2">
							 | 
						|
								                  <div>
							 | 
						|
								                    <p><strong>Camera State:</strong> {{ cameraState }}</p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>State Message:</strong>
							 | 
						|
								                      {{ cameraStateMessage || "None" }}
							 | 
						|
								                    </p>
							 | 
						|
								                    <p><strong>Error:</strong> {{ error || "None" }}</p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>Preview Active:</strong>
							 | 
						|
								                      {{ showCameraPreview ? "Yes" : "No" }}
							 | 
						|
								                    </p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>Stream Active:</strong>
							 | 
						|
								                      {{ !!cameraStream ? "Yes" : "No" }}
							 | 
						|
								                    </p>
							 | 
						|
								                  </div>
							 | 
						|
								                  <div>
							 | 
						|
								                    <p><strong>Browser:</strong> {{ userAgent }}</p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>HTTPS:</strong>
							 | 
						|
								                      {{ isSecureContext ? "Yes" : "No" }}
							 | 
						|
								                    </p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>MediaDevices:</strong>
							 | 
						|
								                      {{ hasMediaDevices ? "Yes" : "No" }}
							 | 
						|
								                    </p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>GetUserMedia:</strong>
							 | 
						|
								                      {{ hasGetUserMedia ? "Yes" : "No" }}
							 | 
						|
								                    </p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>Platform:</strong>
							 | 
						|
								                      {{ platformCapabilities.isMobile ? "Mobile" : "Desktop" }}
							 | 
						|
								                    </p>
							 | 
						|
								                    <p><strong>Camera Mode:</strong> {{ currentFacingMode }}</p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>Video Mirrored:</strong>
							 | 
						|
								                      {{ shouldMirrorVideo ? "Yes" : "No" }}
							 | 
						|
								                    </p>
							 | 
						|
								                    <p>
							 | 
						|
								                      <strong>Mirror Logic:</strong>
							 | 
						|
								                      {{
							 | 
						|
								                        platformCapabilities.isMobile
							 | 
						|
								                          ? "Mobile: user mode only"
							 | 
						|
								                          : "Desktop: always mirrored"
							 | 
						|
								                      }}
							 | 
						|
								                    </p>
							 | 
						|
								                  </div>
							 | 
						|
								                </div>
							 | 
						|
								              </div>
							 | 
						|
								
							 | 
						|
								              <!-- Toggle Diagnostics Button -->
							 | 
						|
								              <button
							 | 
						|
								                :class="diagnosticsToggleClasses"
							 | 
						|
								                @click="toggleDiagnostics"
							 | 
						|
								              >
							 | 
						|
								                {{ showDiagnostics ? "Hide Diagnostics" : "Show Diagnostics" }}
							 | 
						|
								              </button>
							 | 
						|
								              <div class="camera-container w-full h-full relative">
							 | 
						|
								                <video
							 | 
						|
								                  ref="videoElement"
							 | 
						|
								                  class="camera-video w-full h-full object-cover"
							 | 
						|
								                  :class="{ 'mirror-video': shouldMirrorVideo }"
							 | 
						|
								                  autoplay
							 | 
						|
								                  playsinline
							 | 
						|
								                  muted
							 | 
						|
								                ></video>
							 | 
						|
								                <!-- Mirror indicator -->
							 | 
						|
								                <div
							 | 
						|
								                  v-if="shouldMirrorVideo"
							 | 
						|
								                  class="absolute top-2 left-2 bg-black/50 text-white px-2 py-1 rounded text-xs"
							 | 
						|
								                >
							 | 
						|
								                  <font-awesome icon="circle-user" class="w-[1em] mr-1" />
							 | 
						|
								                  Mirrored
							 | 
						|
								                </div>
							 | 
						|
								                <div :class="cameraControlsClasses">
							 | 
						|
								                  <button
							 | 
						|
								                    :class="cameraControlButtonClasses"
							 | 
						|
								                    @click="capturePhoto"
							 | 
						|
								                  >
							 | 
						|
								                    <font-awesome icon="camera" class="w-[1em]" />
							 | 
						|
								                  </button>
							 | 
						|
								                  <button
							 | 
						|
								                    v-if="platformCapabilities.isMobile"
							 | 
						|
								                    :class="cameraControlButtonClasses"
							 | 
						|
								                    @click="rotateCamera"
							 | 
						|
								                  >
							 | 
						|
								                    <font-awesome icon="rotate" class="w-[1em]" />
							 | 
						|
								                  </button>
							 | 
						|
								                </div>
							 | 
						|
								              </div>
							 | 
						|
								            </div>
							 | 
						|
								            <div :class="sectionDividerClasses">
							 | 
						|
								              <span :class="sectionDividerSpanClasses">
							 | 
						|
								                OR choose a file from your device
							 | 
						|
								              </span>
							 | 
						|
								            </div>
							 | 
						|
								            <div class="mt-4">
							 | 
						|
								              <input
							 | 
						|
								                type="file"
							 | 
						|
								                :class="fileInputClasses"
							 | 
						|
								                @change="uploadImageFile"
							 | 
						|
								              />
							 | 
						|
								            </div>
							 | 
						|
								            <div :class="sectionDividerClasses">
							 | 
						|
								              <span :class="sectionDividerSpanClasses">
							 | 
						|
								                OR paste an image URL
							 | 
						|
								              </span>
							 | 
						|
								            </div>
							 | 
						|
								            <div class="flex items-center gap-2 mt-4">
							 | 
						|
								              <input
							 | 
						|
								                v-model="imageUrl"
							 | 
						|
								                type="text"
							 | 
						|
								                :class="urlInputClasses"
							 | 
						|
								                placeholder="https://example.com/image.jpg"
							 | 
						|
								              />
							 | 
						|
								              <button
							 | 
						|
								                v-if="imageUrl"
							 | 
						|
								                :class="acceptUrlButtonClasses"
							 | 
						|
								                @click="acceptUrl"
							 | 
						|
								              >
							 | 
						|
								                <font-awesome icon="check" class="fa-fw" />
							 | 
						|
								              </button>
							 | 
						|
								            </div>
							 | 
						|
								          </div>
							 | 
						|
								
							 | 
						|
								          <div v-else>
							 | 
						|
								            <div v-if="uploading" class="flex justify-center">
							 | 
						|
								              <font-awesome
							 | 
						|
								                icon="spinner"
							 | 
						|
								                class="fa-spin fa-3x text-center block px-12 py-12"
							 | 
						|
								              />
							 | 
						|
								            </div>
							 | 
						|
								            <div v-else>
							 | 
						|
								              <div v-if="crop">
							 | 
						|
								                <VuePictureCropper
							 | 
						|
								                  :box-style="{
							 | 
						|
								                    backgroundColor: '#f8f8f8',
							 | 
						|
								                    margin: 'auto',
							 | 
						|
								                  }"
							 | 
						|
								                  :img="createBlobURL(blob)"
							 | 
						|
								                  :options="{
							 | 
						|
								                    viewMode: 1,
							 | 
						|
								                    dragMode: 'crop',
							 | 
						|
								                    aspectRatio: 1 / 1,
							 | 
						|
								                  }"
							 | 
						|
								                  :class="cropperClasses"
							 | 
						|
								                />
							 | 
						|
								              </div>
							 | 
						|
								              <div v-else>
							 | 
						|
								                <div class="flex justify-center">
							 | 
						|
								                  <img
							 | 
						|
								                    :src="createBlobURL(blob)"
							 | 
						|
								                    :class="imageContainerClasses"
							 | 
						|
								                  />
							 | 
						|
								                </div>
							 | 
						|
								              </div>
							 | 
						|
								              <div :class="buttonGridClasses">
							 | 
						|
								                <button
							 | 
						|
								                  class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
							 | 
						|
								                  @click="uploadImage"
							 | 
						|
								                >
							 | 
						|
								                  <span>Upload</span>
							 | 
						|
								                </button>
							 | 
						|
								                <button
							 | 
						|
								                  v-if="showRetry"
							 | 
						|
								                  class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
							 | 
						|
								                  @click="retryImage"
							 | 
						|
								                >
							 | 
						|
								                  <span>Retry</span>
							 | 
						|
								                </button>
							 | 
						|
								              </div>
							 | 
						|
								            </div>
							 | 
						|
								          </div>
							 | 
						|
								        </template>
							 | 
						|
								        <template v-else>
							 | 
						|
								          <div
							 | 
						|
								            id="noticeBeforeUpload"
							 | 
						|
								            class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3"
							 | 
						|
								            role="alert"
							 | 
						|
								            aria-live="polite"
							 | 
						|
								          >
							 | 
						|
								            <p class="mb-2">
							 | 
						|
								              Before you can upload a photo, a friend needs to register you.
							 | 
						|
								            </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"
							 | 
						|
								              @click="handleQRCodeClick"
							 | 
						|
								            >
							 | 
						|
								              Share Your Info
							 | 
						|
								            </button>
							 | 
						|
								          </div>
							 | 
						|
								        </template>
							 | 
						|
								      </div>
							 | 
						|
								    </div>
							 | 
						|
								  </div>
							 | 
						|
								</template>
							 | 
						|
								
							 | 
						|
								<script lang="ts">
							 | 
						|
								import axios from "axios";
							 | 
						|
								import { ref } from "vue";
							 | 
						|
								import { Component, Vue } from "vue-facing-decorator";
							 | 
						|
								import VuePictureCropper, { cropper } from "vue-picture-cropper";
							 | 
						|
								import { Capacitor } from "@capacitor/core";
							 | 
						|
								import { DEFAULT_IMAGE_API_SERVER } from "../constants/app";
							 | 
						|
								import { accessToken } from "../libs/crypto";
							 | 
						|
								import { logger } from "../utils/logger";
							 | 
						|
								import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
							 | 
						|
								import { Prop } from "vue-facing-decorator";
							 | 
						|
								import { Router } from "vue-router";
							 | 
						|
								import {
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_SETTINGS_ERROR,
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_RETRIEVAL_ERROR,
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_CAPTURE_ERROR,
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_NO_IMAGE_ERROR,
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_UPLOAD_ERROR,
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_AUTH_ERROR,
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_SERVER_ERROR,
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_FILE_TOO_LARGE,
							 | 
						|
								  NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT,
							 | 
						|
								  createImageDialogCameraErrorMessage,
							 | 
						|
								} from "../constants/notifications";
							 | 
						|
								import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "../utils/notify";
							 | 
						|
								
							 | 
						|
								const inputImageFileNameRef = ref<Blob>();
							 | 
						|
								
							 | 
						|
								@Component({
							 | 
						|
								  components: { VuePictureCropper },
							 | 
						|
								  mixins: [PlatformServiceMixin],
							 | 
						|
								})
							 | 
						|
								export default class ImageMethodDialog extends Vue {
							 | 
						|
								  $notify!: NotifyFunction;
							 | 
						|
								  $router!: Router;
							 | 
						|
								  notify!: ReturnType<typeof createNotifyHelpers>;
							 | 
						|
								
							 | 
						|
								  /** Active DID for user authentication */
							 | 
						|
								  activeDid = "";
							 | 
						|
								
							 | 
						|
								  /** Current image blob being processed */
							 | 
						|
								  blob?: Blob;
							 | 
						|
								
							 | 
						|
								  /** Type of claim for the image */
							 | 
						|
								  claimType: string = "";
							 | 
						|
								
							 | 
						|
								  /** Whether to show cropping interface */
							 | 
						|
								  crop: boolean = false;
							 | 
						|
								
							 | 
						|
								  /** Name of the selected file */
							 | 
						|
								  fileName?: string;
							 | 
						|
								
							 | 
						|
								  /** Callback function to set image URL after upload */
							 | 
						|
								  imageCallback: (imageUrl: string) => void = () => {};
							 | 
						|
								
							 | 
						|
								  /** URL for image input */
							 | 
						|
								  imageUrl?: string;
							 | 
						|
								
							 | 
						|
								  /** Whether to show retry button */
							 | 
						|
								  showRetry = true;
							 | 
						|
								
							 | 
						|
								  /** Upload progress state */
							 | 
						|
								  uploading = false;
							 | 
						|
								
							 | 
						|
								  /** Dialog visibility state */
							 | 
						|
								  visible = false;
							 | 
						|
								
							 | 
						|
								  /** Whether to show camera preview */
							 | 
						|
								  showCameraPreview = false;
							 | 
						|
								
							 | 
						|
								  /** Whether currently retrying camera preview */
							 | 
						|
								  isRetrying = false;
							 | 
						|
								
							 | 
						|
								  /** Camera stream reference */
							 | 
						|
								  private cameraStream: MediaStream | null = null;
							 | 
						|
								
							 | 
						|
								  /** Current camera facing mode */
							 | 
						|
								  private currentFacingMode: "environment" | "user" = "environment";
							 | 
						|
								
							 | 
						|
								  /** Platform capabilities (from mixin) */
							 | 
						|
								  get platformCapabilities() {
							 | 
						|
								    return this.platformService.getCapabilities();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Add diagnostic properties
							 | 
						|
								  showDiagnostics = false;
							 | 
						|
								  userAgent = navigator.userAgent;
							 | 
						|
								  isSecureContext = window.isSecureContext;
							 | 
						|
								  hasMediaDevices = !!navigator.mediaDevices;
							 | 
						|
								  hasGetUserMedia = !!(
							 | 
						|
								    navigator.mediaDevices && navigator.mediaDevices.getUserMedia
							 | 
						|
								  );
							 | 
						|
								  cameraState:
							 | 
						|
								    | "off"
							 | 
						|
								    | "initializing"
							 | 
						|
								    | "ready"
							 | 
						|
								    | "active"
							 | 
						|
								    | "error"
							 | 
						|
								    | "permission_denied"
							 | 
						|
								    | "not_found"
							 | 
						|
								    | "in_use" = "off";
							 | 
						|
								  cameraStateMessage?: string;
							 | 
						|
								  error: string | null = null;
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for button grid classes
							 | 
						|
								   * Determines grid layout based on retry button visibility
							 | 
						|
								   */
							 | 
						|
								  get buttonGridClasses(): string {
							 | 
						|
								    return [
							 | 
						|
								      "grid gap-2 mt-2",
							 | 
						|
								      this.showRetry ? "grid-cols-2" : "grid-cols-1",
							 | 
						|
								    ].join(" ");
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for section divider classes
							 | 
						|
								   * Provides consistent styling for section dividers
							 | 
						|
								   */
							 | 
						|
								  get sectionDividerClasses(): string {
							 | 
						|
								    return "border-b border-dashed border-slate-300 text-orange-400 mb-4 font-bold text-sm";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for section divider span classes
							 | 
						|
								   * Provides consistent styling for divider labels
							 | 
						|
								   */
							 | 
						|
								  get sectionDividerSpanClasses(): string {
							 | 
						|
								    return "block w-fit mx-auto -mb-2.5 bg-white px-2";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for camera preview container classes
							 | 
						|
								   * Provides consistent styling for camera preview
							 | 
						|
								   */
							 | 
						|
								  get cameraPreviewClasses(): string {
							 | 
						|
								    return "camera-preview relative flex bg-black overflow-hidden mb-4";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for diagnostics panel classes
							 | 
						|
								   * Provides consistent styling for diagnostics overlay
							 | 
						|
								   */
							 | 
						|
								  get diagnosticsPanelClasses(): string {
							 | 
						|
								    return "absolute top-0 left-0 right-0 bg-black/80 text-white text-xs p-2 pt-8 z-20 overflow-auto max-h-[50vh]";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for diagnostics toggle button classes
							 | 
						|
								   * Provides consistent styling for diagnostics toggle
							 | 
						|
								   */
							 | 
						|
								  get diagnosticsToggleClasses(): string {
							 | 
						|
								    return "absolute top-2 right-2 bg-black/50 text-white px-2 py-1 rounded text-xs z-30";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for camera control button classes
							 | 
						|
								   * Provides consistent styling for camera control buttons
							 | 
						|
								   */
							 | 
						|
								  get cameraControlButtonClasses(): string {
							 | 
						|
								    return "bg-white text-slate-800 p-3 rounded-full text-2xl leading-none";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for camera controls container classes
							 | 
						|
								   * Provides consistent styling for camera controls
							 | 
						|
								   */
							 | 
						|
								  get cameraControlsClasses(): string {
							 | 
						|
								    return "absolute bottom-4 inset-x-0 flex items-center justify-center gap-4";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for file input classes
							 | 
						|
								   * Provides consistent styling for file input with custom button
							 | 
						|
								   */
							 | 
						|
								  get fileInputClasses(): string {
							 | 
						|
								    return "w-full file:text-center file:bg-gradient-to-b file:from-slate-400 file:to-slate-700 file:shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] file:text-white file:px-3 file:py-2 file:rounded-md file:border-none file:cursor-pointer file:me-2";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for URL input classes
							 | 
						|
								   * Provides consistent styling for URL input field
							 | 
						|
								   */
							 | 
						|
								  get urlInputClasses(): string {
							 | 
						|
								    return "block w-full rounded border border-slate-400 px-4 py-2";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for accept URL button classes
							 | 
						|
								   * Provides consistent styling for accept URL button
							 | 
						|
								   */
							 | 
						|
								  get acceptUrlButtonClasses(): string {
							 | 
						|
								    return "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-3 py-2 rounded-md cursor-pointer";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for image container classes
							 | 
						|
								   * Provides consistent styling for image preview
							 | 
						|
								   */
							 | 
						|
								  get imageContainerClasses(): string {
							 | 
						|
								    return "mt-2 rounded max-h-[50vh] max-w-[90vw] object-contain";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property for cropper classes
							 | 
						|
								   * Provides consistent styling for cropper
							 | 
						|
								   */
							 | 
						|
								  get cropperClasses(): string {
							 | 
						|
								    return "max-h-[50vh] max-w-[90vw] object-contain";
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Computed property to determine if video should be mirrored
							 | 
						|
								   * Mirrors video for front-facing camera (user-facing) but not back camera
							 | 
						|
								   * This applies to both desktop and mobile when using front-facing cameras
							 | 
						|
								   */
							 | 
						|
								  get shouldMirrorVideo(): boolean {
							 | 
						|
								    // On desktop, most webcams face the user, so we should mirror regardless of facingMode
							 | 
						|
								    // On mobile, only mirror when using front-facing camera (user mode)
							 | 
						|
								    if (this.platformCapabilities.isMobile) {
							 | 
						|
								      return this.currentFacingMode === "user";
							 | 
						|
								    } else {
							 | 
						|
								      // Desktop: mirror by default since webcams typically face the user
							 | 
						|
								      return true;
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Props
							 | 
						|
								  @Prop({ default: true }) isRegistered!: boolean;
							 | 
						|
								  @Prop({
							 | 
						|
								    default: "environment",
							 | 
						|
								    validator: (value: string) => ["environment", "user"].includes(value),
							 | 
						|
								  })
							 | 
						|
								  defaultCameraMode!: string;
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Lifecycle hook: Initializes component and retrieves user settings
							 | 
						|
								   * @throws {Error} When settings retrieval fails
							 | 
						|
								   */
							 | 
						|
								  async mounted() {
							 | 
						|
								    // Initialize notification helpers
							 | 
						|
								    this.notify = createNotifyHelpers(this.$notify);
							 | 
						|
								
							 | 
						|
								    try {
							 | 
						|
								      // Get activeDid from active_identity table (single source of truth)
							 | 
						|
								      // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								      const activeIdentity = await (this as any).$getActiveIdentity();
							 | 
						|
								      this.activeDid = activeIdentity.activeDid || "";
							 | 
						|
								    } catch (error) {
							 | 
						|
								      logger.error("Error retrieving settings from database:", error);
							 | 
						|
								      this.notify.error(
							 | 
						|
								        NOTIFY_IMAGE_DIALOG_SETTINGS_ERROR.message,
							 | 
						|
								        TIMEOUTS.MODAL,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Lifecycle hook: Cleans up camera stream when component is destroyed
							 | 
						|
								   */
							 | 
						|
								  beforeDestroy() {
							 | 
						|
								    this.stopCameraPreview();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
							 | 
						|
								    logger.debug("ImageMethodDialog.open called");
							 | 
						|
								    this.claimType = claimType;
							 | 
						|
								    this.crop = !!crop;
							 | 
						|
								    this.imageCallback = setImageFn;
							 | 
						|
								    this.visible = true;
							 | 
						|
								
							 | 
						|
								    // Use provided default camera mode, or intelligently choose based on platform
							 | 
						|
								    this.currentFacingMode = this.defaultCameraMode as "environment" | "user";
							 | 
						|
								
							 | 
						|
								    // Log camera mode for debugging
							 | 
						|
								    logger.debug("Camera facing mode:", this.currentFacingMode);
							 | 
						|
								    logger.debug("Should mirror video:", this.shouldMirrorVideo);
							 | 
						|
								    logger.debug("Platform capabilities:", this.platformCapabilities);
							 | 
						|
								
							 | 
						|
								    // Start camera preview immediately
							 | 
						|
								    logger.debug("Starting camera preview from open()");
							 | 
						|
								    this.startCameraPreview();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async uploadImageFile(event: Event) {
							 | 
						|
								    const target = event.target as HTMLInputElement;
							 | 
						|
								    if (!target.files) return;
							 | 
						|
								
							 | 
						|
								    inputImageFileNameRef.value = target.files[0];
							 | 
						|
								    const file = inputImageFileNameRef.value;
							 | 
						|
								    if (file != null) {
							 | 
						|
								      const reader = new FileReader();
							 | 
						|
								      reader.onload = async (e) => {
							 | 
						|
								        const data = e.target?.result as ArrayBuffer;
							 | 
						|
								        if (data) {
							 | 
						|
								          const blob = new Blob([new Uint8Array(data)], {
							 | 
						|
								            type: file.type,
							 | 
						|
								          });
							 | 
						|
								          this.blob = blob;
							 | 
						|
								          this.fileName = (file as File).name;
							 | 
						|
								          this.showRetry = false;
							 | 
						|
								        }
							 | 
						|
								      };
							 | 
						|
								      reader.readAsArrayBuffer(file as Blob);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async acceptUrl() {
							 | 
						|
								    if (this.crop) {
							 | 
						|
								      try {
							 | 
						|
								        const urlBlobResponse = await axios.get(this.imageUrl as string, {
							 | 
						|
								          responseType: "blob",
							 | 
						|
								        });
							 | 
						|
								        const fullUrl = new URL(this.imageUrl as string);
							 | 
						|
								        const fileName = fullUrl.pathname.split("/").pop() as string;
							 | 
						|
								        this.blob = urlBlobResponse.data as Blob;
							 | 
						|
								        this.fileName = fileName;
							 | 
						|
								        this.showRetry = false;
							 | 
						|
								      } catch (error) {
							 | 
						|
								        this.notify.error(
							 | 
						|
								          NOTIFY_IMAGE_DIALOG_RETRIEVAL_ERROR.message,
							 | 
						|
								          TIMEOUTS.LONG,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								    } else {
							 | 
						|
								      this.imageCallback(this.imageUrl as string);
							 | 
						|
								      this.close();
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  close() {
							 | 
						|
								    this.visible = false;
							 | 
						|
								    this.stopCameraPreview();
							 | 
						|
								    const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
							 | 
						|
								    if (bottomNav) {
							 | 
						|
								      bottomNav.style.display = "";
							 | 
						|
								    }
							 | 
						|
								    this.blob = undefined;
							 | 
						|
								    this.showCameraPreview = false;
							 | 
						|
								    this.isRetrying = false;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async startCameraPreview() {
							 | 
						|
								    logger.debug("startCameraPreview called");
							 | 
						|
								    logger.debug("Current showCameraPreview state:", this.showCameraPreview);
							 | 
						|
								    logger.debug("Platform capabilities:", this.platformCapabilities);
							 | 
						|
								    logger.debug("MediaDevices available:", !!navigator.mediaDevices);
							 | 
						|
								    logger.debug(
							 | 
						|
								      "getUserMedia available:",
							 | 
						|
								      !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
							 | 
						|
								    );
							 | 
						|
								    logger.debug("Current facing mode:", this.currentFacingMode);
							 | 
						|
								
							 | 
						|
								    // If camera is already active, don't restart it
							 | 
						|
								    if (this.cameraState === "active" && this.cameraStream) {
							 | 
						|
								      logger.debug("Camera is already active, skipping restart");
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    try {
							 | 
						|
								      this.cameraState = "initializing";
							 | 
						|
								      this.cameraStateMessage = "Requesting camera access...";
							 | 
						|
								      this.showCameraPreview = true;
							 | 
						|
								      await this.$nextTick();
							 | 
						|
								
							 | 
						|
								      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
							 | 
						|
								        throw new Error("Camera API not available in this browser");
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      logger.debug(
							 | 
						|
								        "Requesting camera stream with facingMode:",
							 | 
						|
								        this.currentFacingMode,
							 | 
						|
								      );
							 | 
						|
								      const stream = await navigator.mediaDevices.getUserMedia({
							 | 
						|
								        video: { facingMode: this.currentFacingMode },
							 | 
						|
								      });
							 | 
						|
								      logger.debug(
							 | 
						|
								        "Camera access granted, stream tracks:",
							 | 
						|
								        stream.getTracks().map((t) => ({ kind: t.kind, label: t.label })),
							 | 
						|
								      );
							 | 
						|
								      this.cameraStream = stream;
							 | 
						|
								      this.cameraState = "active";
							 | 
						|
								      this.cameraStateMessage = "Camera is active";
							 | 
						|
								
							 | 
						|
								      await this.$nextTick();
							 | 
						|
								
							 | 
						|
								      const videoElement = this.$refs.videoElement as HTMLVideoElement;
							 | 
						|
								      if (videoElement) {
							 | 
						|
								        logger.debug("Setting video element srcObject");
							 | 
						|
								        videoElement.srcObject = stream;
							 | 
						|
								        await new Promise((resolve, reject) => {
							 | 
						|
								          videoElement.onloadedmetadata = () => {
							 | 
						|
								            logger.debug("Video metadata loaded, starting playback");
							 | 
						|
								            videoElement
							 | 
						|
								              .play()
							 | 
						|
								              .then(() => {
							 | 
						|
								                logger.debug("Video element started playing successfully");
							 | 
						|
								                resolve(true);
							 | 
						|
								              })
							 | 
						|
								              .catch((error) => {
							 | 
						|
								                logger.error("Error playing video:", error);
							 | 
						|
								                reject(error);
							 | 
						|
								              });
							 | 
						|
								          };
							 | 
						|
								          videoElement.onerror = (error) => {
							 | 
						|
								            logger.error("Video element error:", error);
							 | 
						|
								            reject(new Error("Video element error"));
							 | 
						|
								          };
							 | 
						|
								        });
							 | 
						|
								      } else {
							 | 
						|
								        logger.error("Video element not found");
							 | 
						|
								        throw new Error("Video element not found");
							 | 
						|
								      }
							 | 
						|
								    } catch (error) {
							 | 
						|
								      logger.error("Error starting camera preview:", error);
							 | 
						|
								      let errorMessage =
							 | 
						|
								        error instanceof Error ? error.message : "Failed to access camera";
							 | 
						|
								      if (
							 | 
						|
								        error instanceof Error &&
							 | 
						|
								        (error.name === "NotReadableError" || error.name === "TrackStartError")
							 | 
						|
								      ) {
							 | 
						|
								        errorMessage =
							 | 
						|
								          "Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
							 | 
						|
								      } else if (
							 | 
						|
								        error instanceof Error &&
							 | 
						|
								        (error.name === "NotAllowedError" ||
							 | 
						|
								          error.name === "PermissionDeniedError")
							 | 
						|
								      ) {
							 | 
						|
								        errorMessage =
							 | 
						|
								          "Camera access was denied. Please allow camera access in your browser settings.";
							 | 
						|
								      }
							 | 
						|
								      this.cameraState = "error";
							 | 
						|
								      this.cameraStateMessage = errorMessage;
							 | 
						|
								      this.error = errorMessage;
							 | 
						|
								      this.showCameraPreview = false;
							 | 
						|
								      this.notify.error(
							 | 
						|
								        createImageDialogCameraErrorMessage(error as Error),
							 | 
						|
								        TIMEOUTS.LONG,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  stopCameraPreview() {
							 | 
						|
								    logger.debug("stopCameraPreview called");
							 | 
						|
								
							 | 
						|
								    if (this.cameraStream) {
							 | 
						|
								      logger.debug("Stopping camera stream tracks");
							 | 
						|
								      this.cameraStream.getTracks().forEach((track) => {
							 | 
						|
								        track.stop();
							 | 
						|
								        logger.debug(`Stopped track: ${track.kind} - ${track.label}`);
							 | 
						|
								      });
							 | 
						|
								      this.cameraStream = null;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Clear video element srcObject to ensure cleanup
							 | 
						|
								    const videoElement = this.$refs.videoElement as HTMLVideoElement;
							 | 
						|
								    if (videoElement) {
							 | 
						|
								      videoElement.srcObject = null;
							 | 
						|
								      logger.debug("Cleared video element srcObject");
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    this.showCameraPreview = false;
							 | 
						|
								    this.cameraState = "off";
							 | 
						|
								    this.cameraStateMessage = "Camera stopped";
							 | 
						|
								    this.error = null;
							 | 
						|
								    // Don't reset isRetrying here - let the calling method handle it
							 | 
						|
								
							 | 
						|
								    logger.debug("Camera preview stopped and cleaned up");
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async capturePhoto() {
							 | 
						|
								    if (!this.cameraStream) return;
							 | 
						|
								
							 | 
						|
								    try {
							 | 
						|
								      const videoElement = this.$refs.videoElement as HTMLVideoElement;
							 | 
						|
								      const canvas = document.createElement("canvas");
							 | 
						|
								      canvas.width = videoElement.videoWidth;
							 | 
						|
								      canvas.height = videoElement.videoHeight;
							 | 
						|
								      const ctx = canvas.getContext("2d");
							 | 
						|
								
							 | 
						|
								      if (!ctx) {
							 | 
						|
								        throw new Error("Could not get canvas context");
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      // If video is mirrored, flip the canvas horizontally
							 | 
						|
								      // to capture the image in the correct orientation
							 | 
						|
								      if (this.shouldMirrorVideo) {
							 | 
						|
								        ctx.scale(-1, 1);
							 | 
						|
								        ctx.translate(-canvas.width, 0);
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
							 | 
						|
								
							 | 
						|
								      canvas.toBlob(
							 | 
						|
								        (blob) => {
							 | 
						|
								          if (blob) {
							 | 
						|
								            this.blob = blob;
							 | 
						|
								            this.fileName = `photo_${Date.now()}.jpg`;
							 | 
						|
								            this.showRetry = true;
							 | 
						|
								            this.stopCameraPreview();
							 | 
						|
								          }
							 | 
						|
								        },
							 | 
						|
								        "image/jpeg",
							 | 
						|
								        0.95,
							 | 
						|
								      );
							 | 
						|
								    } catch (error) {
							 | 
						|
								      logger.error("Error capturing photo:", error);
							 | 
						|
								      this.notify.error(
							 | 
						|
								        NOTIFY_IMAGE_DIALOG_CAPTURE_ERROR.message,
							 | 
						|
								        TIMEOUTS.LONG,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async rotateCamera() {
							 | 
						|
								    // Toggle between front and back cameras
							 | 
						|
								    this.currentFacingMode =
							 | 
						|
								      this.currentFacingMode === "environment" ? "user" : "environment";
							 | 
						|
								
							 | 
						|
								    // Stop current stream
							 | 
						|
								    if (this.cameraStream) {
							 | 
						|
								      this.cameraStream.getTracks().forEach((track) => track.stop());
							 | 
						|
								      this.cameraStream = null;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Start new stream with updated facing mode
							 | 
						|
								    await this.startCameraPreview();
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  private createBlobURL(blob: Blob): string {
							 | 
						|
								    return URL.createObjectURL(blob);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async retryImage() {
							 | 
						|
								    // Set retry flag FIRST to prevent error message flash
							 | 
						|
								    this.isRetrying = true;
							 | 
						|
								
							 | 
						|
								    this.blob = undefined;
							 | 
						|
								    this.showRetry = true;
							 | 
						|
								
							 | 
						|
								    // Stop camera stream but keep showCameraPreview true to prevent error flash
							 | 
						|
								    if (this.cameraStream) {
							 | 
						|
								      logger.debug("Stopping camera stream tracks");
							 | 
						|
								      this.cameraStream.getTracks().forEach((track) => {
							 | 
						|
								        track.stop();
							 | 
						|
								        logger.debug(`Stopped track: ${track.kind} - ${track.label}`);
							 | 
						|
								      });
							 | 
						|
								      this.cameraStream = null;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    // Clear video element srcObject to ensure cleanup
							 | 
						|
								    const videoElement = this.$refs.videoElement as HTMLVideoElement;
							 | 
						|
								    if (videoElement) {
							 | 
						|
								      videoElement.srcObject = null;
							 | 
						|
								      logger.debug("Cleared video element srcObject");
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    this.cameraState = "off";
							 | 
						|
								    this.cameraStateMessage = "Camera stopped";
							 | 
						|
								    this.error = null;
							 | 
						|
								
							 | 
						|
								    // Add a small delay to ensure cleanup is complete
							 | 
						|
								    await new Promise((resolve) => setTimeout(resolve, 100));
							 | 
						|
								
							 | 
						|
								    // For native apps (iOS/Android), we need to restart the camera preview
							 | 
						|
								    // For web browsers, we also restart the camera preview
							 | 
						|
								    await this.startCameraPreview();
							 | 
						|
								
							 | 
						|
								    // Reset retry flag after camera is started
							 | 
						|
								    this.isRetrying = false;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  async uploadImage() {
							 | 
						|
								    this.uploading = true;
							 | 
						|
								
							 | 
						|
								    if (this.crop) {
							 | 
						|
								      this.blob = (await cropper?.getBlob()) || undefined;
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    const token = await accessToken(this.activeDid);
							 | 
						|
								    const headers = {
							 | 
						|
								      Authorization: "Bearer " + token,
							 | 
						|
								    };
							 | 
						|
								    const formData = new FormData();
							 | 
						|
								    if (!this.blob) {
							 | 
						|
								      this.notify.error(
							 | 
						|
								        NOTIFY_IMAGE_DIALOG_NO_IMAGE_ERROR.message,
							 | 
						|
								        TIMEOUTS.LONG,
							 | 
						|
								      );
							 | 
						|
								      this.uploading = false;
							 | 
						|
								      this.close();
							 | 
						|
								      return;
							 | 
						|
								    }
							 | 
						|
								    formData.append("image", this.blob, this.fileName || "photo.jpg");
							 | 
						|
								    formData.append("claimType", this.claimType);
							 | 
						|
								    try {
							 | 
						|
								      if (
							 | 
						|
								        window.location.hostname === "localhost" &&
							 | 
						|
								        !DEFAULT_IMAGE_API_SERVER.includes("localhost")
							 | 
						|
								      ) {
							 | 
						|
								        logger.log(
							 | 
						|
								          "Using shared image API server, so only users on that server can play with images.",
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								      const response = await axios.post(
							 | 
						|
								        DEFAULT_IMAGE_API_SERVER + "/image",
							 | 
						|
								        formData,
							 | 
						|
								        { headers },
							 | 
						|
								      );
							 | 
						|
								      this.uploading = false;
							 | 
						|
								
							 | 
						|
								      this.close();
							 | 
						|
								      this.imageCallback(response.data.url as string);
							 | 
						|
								    } catch (error: unknown) {
							 | 
						|
								      let errorMessage = "There was an error saving the picture.";
							 | 
						|
								
							 | 
						|
								      if (axios.isAxiosError(error)) {
							 | 
						|
								        const status = error.response?.status;
							 | 
						|
								        const data = error.response?.data;
							 | 
						|
								
							 | 
						|
								        if (status === 401) {
							 | 
						|
								          this.notify.error(
							 | 
						|
								            NOTIFY_IMAGE_DIALOG_AUTH_ERROR.message,
							 | 
						|
								            TIMEOUTS.LONG,
							 | 
						|
								          );
							 | 
						|
								        } else if (status === 413) {
							 | 
						|
								          this.notify.error(
							 | 
						|
								            NOTIFY_IMAGE_DIALOG_FILE_TOO_LARGE.message,
							 | 
						|
								            TIMEOUTS.LONG,
							 | 
						|
								          );
							 | 
						|
								        } else if (status === 415) {
							 | 
						|
								          this.notify.error(
							 | 
						|
								            NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT.message,
							 | 
						|
								            TIMEOUTS.LONG,
							 | 
						|
								          );
							 | 
						|
								        } else if (status && status >= 500) {
							 | 
						|
								          this.notify.error(
							 | 
						|
								            NOTIFY_IMAGE_DIALOG_SERVER_ERROR.message,
							 | 
						|
								            TIMEOUTS.LONG,
							 | 
						|
								          );
							 | 
						|
								        } else if (data?.message) {
							 | 
						|
								          errorMessage = data.message;
							 | 
						|
								          this.notify.error(errorMessage, TIMEOUTS.LONG);
							 | 
						|
								        } else {
							 | 
						|
								          this.notify.error(
							 | 
						|
								            NOTIFY_IMAGE_DIALOG_UPLOAD_ERROR.message,
							 | 
						|
								            TIMEOUTS.LONG,
							 | 
						|
								          );
							 | 
						|
								        }
							 | 
						|
								      } else {
							 | 
						|
								        this.notify.error(
							 | 
						|
								          NOTIFY_IMAGE_DIALOG_UPLOAD_ERROR.message,
							 | 
						|
								          TIMEOUTS.LONG,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								
							 | 
						|
								      this.uploading = false;
							 | 
						|
								      this.blob = undefined;
							 | 
						|
								      this.close();
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Add toggle method
							 | 
						|
								  toggleDiagnostics() {
							 | 
						|
								    this.showDiagnostics = !this.showDiagnostics;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  private handleQRCodeClick() {
							 | 
						|
								    if (Capacitor.isNativePlatform()) {
							 | 
						|
								      this.$router.push({ name: "contact-qr-scan-full" });
							 | 
						|
								    } else {
							 | 
						|
								      this.$router.push({ name: "contact-qr" });
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								</script>
							 | 
						|
								
							 | 
						|
								<style>
							 | 
						|
								/* Add styles for diagnostic panel */
							 | 
						|
								.diagnostic-panel {
							 | 
						|
								  font-family: monospace;
							 | 
						|
								  white-space: pre-wrap;
							 | 
						|
								  word-break: break-all;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/* Mirror video for front-facing camera */
							 | 
						|
								.mirror-video {
							 | 
						|
								  transform: scaleX(-1);
							 | 
						|
								}
							 | 
						|
								</style>
							 | 
						|
								
							 |