diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue index 65d41e6b..342bf5d4 100644 --- a/src/components/PhotoDialog.vue +++ b/src/components/PhotoDialog.vue @@ -15,15 +15,16 @@ PhotoDialog.vue */
Uploading... Look Good? + Take Photo Say "Cheese"!
@@ -47,7 +48,7 @@ PhotoDialog.vue */ :options="{ viewMode: 1, dragMode: 'crop', - aspectRatio: 9 / 9, + aspectRatio: 1 / 1, }" class="max-h-[90vh] max-w-[90vw] object-contain" /> @@ -60,32 +61,45 @@ PhotoDialog.vue */ />
-
+
-
-
+
+
+ + +
+
@@ -142,20 +156,29 @@ export default class PhotoDialog extends Vue { /** Dialog visibility state */ visible = false; + /** Whether to show camera preview */ + showCameraPreview = false; + + /** Camera stream reference */ + private cameraStream: MediaStream | null = null; + private platformService = PlatformServiceFactory.getInstance(); URL = window.URL || window.webkitURL; isRegistered = false; + private platformCapabilities = this.platformService.getCapabilities(); /** * Lifecycle hook: Initializes component and retrieves user settings * @throws {Error} When settings retrieval fails */ async mounted() { + console.log('PhotoDialog mounted'); try { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; this.isRegistered = !!settings.isRegistered; + console.log('isRegistered:', this.isRegistered); } catch (error: unknown) { logger.error("Error retrieving settings from database:", error); this.$notify( @@ -173,6 +196,13 @@ export default class PhotoDialog extends Vue { } } + /** + * Lifecycle hook: Cleans up camera stream when component is destroyed + */ + beforeDestroy() { + this.stopCameraPreview(); + } + /** * Opens the photo dialog with specified configuration * @param setImageFn - Callback function to handle image URL after upload @@ -181,7 +211,7 @@ export default class PhotoDialog extends Vue { * @param blob - Optional existing image blob * @param inputFileName - Optional filename for the image */ - open( + async open( setImageFn: (arg: string) => void, claimType: string, crop?: boolean, @@ -204,6 +234,10 @@ export default class PhotoDialog extends Vue { this.blob = undefined; this.fileName = undefined; this.showRetry = true; + // Start camera preview automatically if no blob is provided + if (!this.platformCapabilities.isMobile) { + await this.startCameraPreview(); + } } } @@ -211,7 +245,9 @@ export default class PhotoDialog extends Vue { * Closes the photo dialog and resets state */ close() { + logger.debug("Dialog closing, current showCameraPreview:", this.showCameraPreview); this.visible = false; + this.stopCameraPreview(); const bottomNav = document.querySelector("#QuickNav") as HTMLElement; if (bottomNav) { bottomNav.style.display = ""; @@ -219,6 +255,138 @@ export default class PhotoDialog extends Vue { this.blob = undefined; } + /** + * Starts the camera preview + */ + async startCameraPreview() { + logger.debug("startCameraPreview called"); + logger.debug("Current showCameraPreview state:", this.showCameraPreview); + logger.debug("Platform capabilities:", this.platformCapabilities); + + // If we're on a mobile device or using Capacitor, use the platform service + if (this.platformCapabilities.isMobile) { + logger.debug("Using platform service for mobile device"); + try { + const result = await this.platformService.takePicture(); + this.blob = result.blob; + this.fileName = result.fileName; + } catch (error) { + logger.error("Error taking picture:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to take picture. Please try again.", + }, + 5000, + ); + } + return; + } + + // For desktop web browsers, use our custom preview + logger.debug("Starting camera preview for desktop browser"); + try { + // Set state before requesting camera access + this.showCameraPreview = true; + logger.debug("showCameraPreview set to:", this.showCameraPreview); + + // Force a re-render + await this.$nextTick(); + logger.debug("After nextTick, showCameraPreview is:", this.showCameraPreview); + + logger.debug("Requesting camera access..."); + const stream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: "environment" }, + }); + logger.debug("Camera access granted, setting up video element"); + this.cameraStream = stream; + + // Force another re-render after getting the stream + await this.$nextTick(); + logger.debug("After getting stream, showCameraPreview is:", this.showCameraPreview); + + const videoElement = this.$refs.videoElement as HTMLVideoElement; + if (videoElement) { + logger.debug("Video element found, setting srcObject"); + videoElement.srcObject = stream; + // Wait for video to be ready + await new Promise((resolve) => { + videoElement.onloadedmetadata = () => { + logger.debug("Video metadata loaded"); + videoElement.play().then(() => { + logger.debug("Video playback started"); + resolve(true); + }); + }; + }); + } else { + logger.error("Video element not found"); + } + } catch (error) { + logger.error("Error starting camera preview:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to access camera. Please try again.", + }, + 5000, + ); + this.showCameraPreview = false; + } + } + + /** + * Stops the camera preview and cleans up resources + */ + stopCameraPreview() { + logger.debug("Stopping camera preview, current showCameraPreview:", this.showCameraPreview); + if (this.cameraStream) { + this.cameraStream.getTracks().forEach((track) => track.stop()); + this.cameraStream = null; + } + this.showCameraPreview = false; + logger.debug("After stopping, showCameraPreview is:", this.showCameraPreview); + } + + /** + * Captures a photo from the camera preview + */ + 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"); + ctx?.drawImage(videoElement, 0, 0, canvas.width, canvas.height); + + canvas.toBlob((blob) => { + if (blob) { + this.blob = blob; + this.fileName = `photo_${Date.now()}.jpg`; + this.stopCameraPreview(); + } + }, "image/jpeg", 0.95); + } catch (error) { + logger.error("Error capturing photo:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: "Failed to capture photo. Please try again.", + }, + 5000, + ); + } + } + /** * Captures a photo using device camera * @throws {Error} When camera access fails @@ -275,10 +443,13 @@ export default class PhotoDialog extends Vue { } /** - * Resets the current image selection + * Resets the current image selection and restarts camera preview */ async retryImage() { this.blob = undefined; + if (!this.platformCapabilities.isMobile) { + await this.startCameraPreview(); + } } /** @@ -422,5 +593,43 @@ export default class PhotoDialog extends Vue { border-radius: 0.5rem; width: 100%; max-width: 700px; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; +} + +/* Camera preview styling */ +.camera-preview { + flex: 1; + background-color: #000; + overflow: hidden; + position: relative; +} + +.camera-container { + width: 100%; + height: 100%; + position: relative; +} + +.camera-video { + width: 100%; + height: 100%; + object-fit: cover; +} + +.capture-button { + position: absolute; + bottom: 1rem; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(to bottom, #60a5fa, #2563eb); + color: white; + padding: 0.75rem 1.5rem; + border-radius: 9999px; + box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.5); + border: none; + cursor: pointer; } diff --git a/src/lib/fontawesome.ts b/src/lib/fontawesome.ts index 181bcb15..37b5343c 100644 --- a/src/lib/fontawesome.ts +++ b/src/lib/fontawesome.ts @@ -54,6 +54,7 @@ import { faHandHoldingDollar, faHandHoldingHeart, faHouseChimney, + faImage, faImagePortrait, faLeftRight, faLightbulb, @@ -135,6 +136,7 @@ library.add( faHandHoldingDollar, faHandHoldingHeart, faHouseChimney, + faImage, faImagePortrait, faLeftRight, faLightbulb,