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,