|
|
@ -15,15 +15,16 @@ PhotoDialog.vue */ |
|
|
|
<div class="text-lg text-center font-light relative z-50"> |
|
|
|
<div |
|
|
|
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-else-if="blob"> Look Good? </span> |
|
|
|
<span v-else-if="showCameraPreview"> Take Photo </span> |
|
|
|
<span v-else> Say "Cheese"! </span> |
|
|
|
</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()" |
|
|
|
> |
|
|
|
<font-awesome icon="xmark" class="w-[1em]"></font-awesome> |
|
|
@ -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 */ |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1"> |
|
|
|
<div class="grid grid-cols-2 gap-2 mt-2"> |
|
|
|
<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" |
|
|
|
> |
|
|
|
<span>Upload</span> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
v-if="showRetry" |
|
|
|
class="absolute bottom-[1rem] right-[1rem] px-2 py-1" |
|
|
|
> |
|
|
|
<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" |
|
|
|
> |
|
|
|
<span>Retry</span> |
|
|
|
</button> |
|
|
|
</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 class="flex flex-col items-center justify-center gap-4 p-4"> |
|
|
|
<button |
|
|
|
v-if="isRegistered" |
|
|
|
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]" /> |
|
|
|
</button> |
|
|
@ -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; |
|
|
|
} |
|
|
|
</style> |
|
|
|