forked from trent_larson/crowd-funder-for-time-pwa
Fix: current photo dialog
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user