Browse Source

Fix: current photo dialog

qrcode-reboot
Jose Olarte III 1 day ago
parent
commit
70174aea93
  1. 237
      src/components/PhotoDialog.vue
  2. 2
      src/lib/fontawesome.ts

237
src/components/PhotoDialog.vue

@ -15,15 +15,16 @@ PhotoDialog.vue */
<div class="text-lg text-center font-light relative z-50"> <div class="text-lg text-center font-light relative z-50">
<div <div
id="ViewHeading" id="ViewHeading"
class="text-center font-bold absolute top-0 left-0 right-0 px-4 py-0.5 bg-black/50 text-white leading-none" class="text-center font-bold absolute top-0 inset-x-0 px-4 py-2 bg-black/50 text-white leading-none pointer-events-none"
> >
<span v-if="uploading"> Uploading... </span> <span v-if="uploading"> Uploading... </span>
<span v-else-if="blob"> Look Good? </span> <span v-else-if="blob"> Look Good? </span>
<span v-else-if="showCameraPreview"> Take Photo </span>
<span v-else> Say "Cheese"! </span> <span v-else> Say "Cheese"! </span>
</div> </div>
<div <div
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white" class="text-lg text-center px-2 py-2 leading-none absolute right-0 top-0 text-white cursor-pointer"
@click="close()" @click="close()"
> >
<font-awesome icon="xmark" class="w-[1em]"></font-awesome> <font-awesome icon="xmark" class="w-[1em]"></font-awesome>
@ -47,7 +48,7 @@ PhotoDialog.vue */
:options="{ :options="{
viewMode: 1, viewMode: 1,
dragMode: 'crop', dragMode: 'crop',
aspectRatio: 9 / 9, aspectRatio: 1 / 1,
}" }"
class="max-h-[90vh] max-w-[90vw] object-contain" class="max-h-[90vh] max-w-[90vw] object-contain"
/> />
@ -60,32 +61,45 @@ PhotoDialog.vue */
/> />
</div> </div>
</div> </div>
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1"> <div class="grid grid-cols-2 gap-2 mt-2">
<button <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-1 px-2 rounded-md" 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" @click="uploadImage"
> >
<span>Upload</span> <span>Upload</span>
</button> </button>
</div>
<div
v-if="showRetry"
class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
>
<button <button
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-1 px-2 rounded-md" 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" @click="retryImage"
> >
<span>Retry</span> <span>Retry</span>
</button> </button>
</div> </div>
</div> </div>
<div v-else-if="showCameraPreview" class="camera-preview">
<div class="camera-container">
<video
ref="videoElement"
class="camera-video"
autoplay
playsinline
muted
></video>
<button
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
@click="capturePhoto"
>
<font-awesome icon="camera" class="w-[1em]" />
</button>
</div>
</div>
<div v-else> <div v-else>
<div class="flex flex-col items-center justify-center gap-4 p-4"> <div class="flex flex-col items-center justify-center gap-4 p-4">
<button <button
v-if="isRegistered" v-if="isRegistered"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none" class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="takePhoto" @click="startCameraPreview"
> >
<font-awesome icon="camera" class="w-[1em]" /> <font-awesome icon="camera" class="w-[1em]" />
</button> </button>
@ -142,20 +156,29 @@ export default class PhotoDialog extends Vue {
/** Dialog visibility state */ /** Dialog visibility state */
visible = false; visible = false;
/** Whether to show camera preview */
showCameraPreview = false;
/** Camera stream reference */
private cameraStream: MediaStream | null = null;
private platformService = PlatformServiceFactory.getInstance(); private platformService = PlatformServiceFactory.getInstance();
URL = window.URL || window.webkitURL; URL = window.URL || window.webkitURL;
isRegistered = false; isRegistered = false;
private platformCapabilities = this.platformService.getCapabilities();
/** /**
* Lifecycle hook: Initializes component and retrieves user settings * Lifecycle hook: Initializes component and retrieves user settings
* @throws {Error} When settings retrieval fails * @throws {Error} When settings retrieval fails
*/ */
async mounted() { async mounted() {
console.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);
} 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(
@ -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 * Opens the photo dialog with specified configuration
* @param setImageFn - Callback function to handle image URL after upload * @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 blob - Optional existing image blob
* @param inputFileName - Optional filename for the image * @param inputFileName - Optional filename for the image
*/ */
open( async open(
setImageFn: (arg: string) => void, setImageFn: (arg: string) => void,
claimType: string, claimType: string,
crop?: boolean, crop?: boolean,
@ -204,6 +234,10 @@ export default class PhotoDialog extends Vue {
this.blob = undefined; this.blob = undefined;
this.fileName = undefined; this.fileName = undefined;
this.showRetry = true; 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 * Closes the photo dialog and resets state
*/ */
close() { close() {
logger.debug("Dialog closing, current showCameraPreview:", this.showCameraPreview);
this.visible = false; this.visible = false;
this.stopCameraPreview();
const bottomNav = document.querySelector("#QuickNav") as HTMLElement; const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
if (bottomNav) { if (bottomNav) {
bottomNav.style.display = ""; bottomNav.style.display = "";
@ -219,6 +255,138 @@ export default class PhotoDialog extends Vue {
this.blob = undefined; 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 * Captures a photo using device camera
* @throws {Error} When camera access fails * @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() { async retryImage() {
this.blob = undefined; this.blob = undefined;
if (!this.platformCapabilities.isMobile) {
await this.startCameraPreview();
}
} }
/** /**
@ -422,5 +593,43 @@ export default class PhotoDialog extends Vue {
border-radius: 0.5rem; border-radius: 0.5rem;
width: 100%; width: 100%;
max-width: 700px; 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;
} }
</style> </style>

2
src/lib/fontawesome.ts

@ -54,6 +54,7 @@ import {
faHandHoldingDollar, faHandHoldingDollar,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faImage,
faImagePortrait, faImagePortrait,
faLeftRight, faLeftRight,
faLightbulb, faLightbulb,
@ -135,6 +136,7 @@ library.add(
faHandHoldingDollar, faHandHoldingDollar,
faHandHoldingHeart, faHandHoldingHeart,
faHouseChimney, faHouseChimney,
faImage,
faImagePortrait, faImagePortrait,
faLeftRight, faLeftRight,
faLightbulb, faLightbulb,

Loading…
Cancel
Save