|
|
@ -1,79 +1,176 @@ |
|
|
|
<template> |
|
|
|
<div v-if="visible" class="dialog-overlay z-[60]"> |
|
|
|
<div class="dialog relative"> |
|
|
|
<div class="text-lg text-center font-light relative z-50"> |
|
|
|
<div |
|
|
|
<div class="text-lg text-center font-bold relative"> |
|
|
|
<h1 |
|
|
|
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" |
|
|
|
> |
|
|
|
Add Photo |
|
|
|
</div> |
|
|
|
<span v-if="uploading">Uploading Image…</span> |
|
|
|
<span v-else-if="blob">Crop Image</span> |
|
|
|
<span v-else-if="showCameraPreview">Upload Image</span> |
|
|
|
<span v-else>Add Photo</span> |
|
|
|
</h1> |
|
|
|
<div |
|
|
|
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white" |
|
|
|
class="text-2xl text-center px-1 py-0.5 leading-none absolute -right-1 top-0" |
|
|
|
@click="close()" |
|
|
|
> |
|
|
|
<font-awesome icon="xmark" class="w-[1em]"></font-awesome> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div> |
|
|
|
<div class="text-center mt-8"> |
|
|
|
<template v-if="isRegistered"> |
|
|
|
<div> |
|
|
|
<font-awesome |
|
|
|
icon="camera" |
|
|
|
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 px-2 py-2 rounded-md" |
|
|
|
@click="openPhotoDialog()" |
|
|
|
/> |
|
|
|
<div class="mt-4"> |
|
|
|
<template v-if="isRegistered"> |
|
|
|
<div v-if="!blob"> |
|
|
|
<div |
|
|
|
class="border-b border-dashed border-slate-300 text-orange-400 mb-4 font-bold text-sm" |
|
|
|
> |
|
|
|
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2"> |
|
|
|
Take a photo with your camera |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<div class="mt-4"> |
|
|
|
<input type="file" @change="uploadImageFile" /> |
|
|
|
<div v-if="showCameraPreview" class="camera-preview relative flex bg-black overflow-hidden mb-4"> |
|
|
|
<div class="camera-container w-full h-full relative"> |
|
|
|
<video |
|
|
|
ref="videoElement" |
|
|
|
class="camera-video w-full h-full object-cover" |
|
|
|
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 class="mt-4"> |
|
|
|
<span class="mt-2"> |
|
|
|
... or paste a URL: |
|
|
|
<input v-model="imageUrl" type="text" class="border-2" /> |
|
|
|
<div |
|
|
|
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-4 font-bold text-sm" |
|
|
|
> |
|
|
|
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2"> |
|
|
|
OR choose a file from your device |
|
|
|
</span> |
|
|
|
<span class="ml-2"> |
|
|
|
<font-awesome |
|
|
|
v-if="imageUrl" |
|
|
|
icon="check" |
|
|
|
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 px-2 py-2 rounded-md cursor-pointer" |
|
|
|
@click="acceptUrl" |
|
|
|
/> |
|
|
|
<!-- so that there's no shifting when it becomes visible --> |
|
|
|
<font-awesome |
|
|
|
v-else |
|
|
|
icon="check" |
|
|
|
class="text-white bg-white px-2 py-2" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
<div class="mt-4"> |
|
|
|
<input |
|
|
|
type="file" |
|
|
|
@change="uploadImageFile" |
|
|
|
class="w-full file:text-center file:bg-gradient-to-b file:from-slate-400 file:to-slate-700 file:shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] file:text-white file:px-3 file:py-2 file:rounded-md file:border-none file:cursor-pointer file:me-2" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-4 font-bold text-sm" |
|
|
|
> |
|
|
|
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2"> |
|
|
|
OR paste an image URL |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
<template v-else> |
|
|
|
<div class="text-center text-lg text-slate-500 py-12"> |
|
|
|
Register to Upload a Photo |
|
|
|
<div class="flex items-center gap-2 mt-4"> |
|
|
|
<input |
|
|
|
v-model="imageUrl" |
|
|
|
type="text" |
|
|
|
class="block w-full rounded border border-slate-400 px-4 py-2" |
|
|
|
placeholder="https://example.com/image.jpg" |
|
|
|
/> |
|
|
|
<button |
|
|
|
v-if="imageUrl" |
|
|
|
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 px-3 py-2 rounded-md cursor-pointer" |
|
|
|
@click="acceptUrl" |
|
|
|
> |
|
|
|
<font-awesome icon="check" class="fa-fw" /> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-else> |
|
|
|
<div v-if="uploading" class="flex justify-center"> |
|
|
|
<font-awesome |
|
|
|
icon="spinner" |
|
|
|
class="fa-spin fa-3x text-center block px-12 py-12" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
<div v-else> |
|
|
|
<div v-if="crop"> |
|
|
|
<VuePictureCropper |
|
|
|
:box-style="{ |
|
|
|
backgroundColor: '#f8f8f8', |
|
|
|
margin: 'auto', |
|
|
|
}" |
|
|
|
:img="createBlobURL(blob)" |
|
|
|
:options="{ |
|
|
|
viewMode: 1, |
|
|
|
dragMode: 'crop', |
|
|
|
aspectRatio: 1 / 1, |
|
|
|
}" |
|
|
|
class="max-h-[90vh] max-w-[90vw] object-contain" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
<div v-else> |
|
|
|
<div class="flex justify-center"> |
|
|
|
<img |
|
|
|
:src="createBlobURL(blob)" |
|
|
|
class="mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<div :class="['grid gap-2 mt-2', showRetry ? 'grid-cols-2' : 'grid-cols-1']"> |
|
|
|
<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-2 px-3 rounded-md" |
|
|
|
@click="uploadImage" |
|
|
|
> |
|
|
|
<span>Upload</span> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
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> |
|
|
|
</template> |
|
|
|
<template v-else> |
|
|
|
<div |
|
|
|
id="noticeBeforeUpload" |
|
|
|
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3" |
|
|
|
role="alert" |
|
|
|
aria-live="polite" |
|
|
|
> |
|
|
|
<p class="mb-2"> |
|
|
|
Before you can upload a photo, a friend needs to register you. |
|
|
|
</p> |
|
|
|
<router-link |
|
|
|
:to="{ name: 'contact-qr' }" |
|
|
|
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" |
|
|
|
> |
|
|
|
Share Your Info |
|
|
|
</router-link> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<PhotoDialog ref="photoDialog" /> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script lang="ts"> |
|
|
|
import axios from "axios"; |
|
|
|
import { ref } from "vue"; |
|
|
|
import { Component, Vue } from "vue-facing-decorator"; |
|
|
|
|
|
|
|
import PhotoDialog from "../components/PhotoDialog.vue"; |
|
|
|
import { NotificationIface } from "../constants/app"; |
|
|
|
import VuePictureCropper, { cropper } from "vue-picture-cropper"; |
|
|
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app"; |
|
|
|
import { retrieveSettingsForActiveAccount } from "../db/index"; |
|
|
|
import { accessToken } from "../libs/crypto"; |
|
|
|
import { logger } from "../utils/logger"; |
|
|
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; |
|
|
|
|
|
|
|
const inputImageFileNameRef = ref<Blob>(); |
|
|
|
|
|
|
|
@Component({ |
|
|
|
components: { PhotoDialog }, |
|
|
|
components: { VuePictureCropper }, |
|
|
|
props: { |
|
|
|
isRegistered: { |
|
|
|
type: Boolean, |
|
|
@ -84,38 +181,97 @@ const inputImageFileNameRef = ref<Blob>(); |
|
|
|
export default class ImageMethodDialog extends Vue { |
|
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void; |
|
|
|
|
|
|
|
claimType: string; |
|
|
|
/** Active DID for user authentication */ |
|
|
|
activeDid = ""; |
|
|
|
|
|
|
|
/** Current image blob being processed */ |
|
|
|
blob?: Blob; |
|
|
|
|
|
|
|
/** Type of claim for the image */ |
|
|
|
claimType: string = ""; |
|
|
|
|
|
|
|
/** Whether to show cropping interface */ |
|
|
|
crop: boolean = false; |
|
|
|
|
|
|
|
/** Name of the selected file */ |
|
|
|
fileName?: string; |
|
|
|
|
|
|
|
/** Callback function to set image URL after upload */ |
|
|
|
imageCallback: (imageUrl?: string) => void = () => {}; |
|
|
|
|
|
|
|
/** URL for image input */ |
|
|
|
imageUrl?: string; |
|
|
|
|
|
|
|
/** Whether to show retry button */ |
|
|
|
showRetry = true; |
|
|
|
|
|
|
|
/** Upload progress state */ |
|
|
|
uploading = false; |
|
|
|
|
|
|
|
/** 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; |
|
|
|
|
|
|
|
private platformCapabilities = this.platformService.getCapabilities(); |
|
|
|
|
|
|
|
/** |
|
|
|
* Lifecycle hook: Initializes component and retrieves user settings |
|
|
|
* @throws {Error} When settings retrieval fails |
|
|
|
*/ |
|
|
|
async mounted() { |
|
|
|
console.log('ImageMethodDialog mounted'); |
|
|
|
try { |
|
|
|
const settings = await retrieveSettingsForActiveAccount(); |
|
|
|
this.activeDid = settings.activeDid || ""; |
|
|
|
} catch (error: unknown) { |
|
|
|
logger.error("Error retrieving settings from database:", error); |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Error", |
|
|
|
text: |
|
|
|
error instanceof Error |
|
|
|
? error.message |
|
|
|
: "There was an error retrieving your settings.", |
|
|
|
}, |
|
|
|
-1, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Lifecycle hook: Cleans up camera stream when component is destroyed |
|
|
|
*/ |
|
|
|
beforeDestroy() { |
|
|
|
this.stopCameraPreview(); |
|
|
|
} |
|
|
|
|
|
|
|
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) { |
|
|
|
this.claimType = claimType; |
|
|
|
this.crop = !!crop; |
|
|
|
this.imageCallback = setImageFn; |
|
|
|
|
|
|
|
this.visible = true; |
|
|
|
} |
|
|
|
|
|
|
|
openPhotoDialog(blob?: Blob, fileName?: string) { |
|
|
|
this.visible = false; |
|
|
|
|
|
|
|
(this.$refs.photoDialog as PhotoDialog).open( |
|
|
|
this.imageCallback, |
|
|
|
this.claimType, |
|
|
|
this.crop, |
|
|
|
blob, |
|
|
|
fileName, |
|
|
|
); |
|
|
|
|
|
|
|
// Start camera preview immediately if not on mobile |
|
|
|
if (!this.platformCapabilities.isMobile) { |
|
|
|
this.startCameraPreview(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async uploadImageFile(event: Event) { |
|
|
|
this.visible = false; |
|
|
|
const target = event.target as HTMLInputElement; |
|
|
|
if (!target.files) return; |
|
|
|
|
|
|
|
inputImageFileNameRef.value = event.target.files[0]; |
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/File |
|
|
|
// ... plus it has a `type` property from my testing |
|
|
|
inputImageFileNameRef.value = target.files[0]; |
|
|
|
const file = inputImageFileNameRef.value; |
|
|
|
if (file != null) { |
|
|
|
const reader = new FileReader(); |
|
|
@ -125,7 +281,9 @@ export default class ImageMethodDialog extends Vue { |
|
|
|
const blob = new Blob([new Uint8Array(data)], { |
|
|
|
type: file.type, |
|
|
|
}); |
|
|
|
this.openPhotoDialog(blob, file.name as string); |
|
|
|
this.blob = blob; |
|
|
|
this.fileName = file.name; |
|
|
|
this.showRetry = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
reader.readAsArrayBuffer(file as Blob); |
|
|
@ -133,21 +291,16 @@ export default class ImageMethodDialog extends Vue { |
|
|
|
} |
|
|
|
|
|
|
|
async acceptUrl() { |
|
|
|
this.visible = false; |
|
|
|
if (this.crop) { |
|
|
|
try { |
|
|
|
const urlBlobResponse: Blob = await axios.get(this.imageUrl as string, { |
|
|
|
responseType: "blob", // This ensures the data is returned as a Blob |
|
|
|
const urlBlobResponse = await axios.get(this.imageUrl as string, { |
|
|
|
responseType: "blob", |
|
|
|
}); |
|
|
|
const fullUrl = new URL(this.imageUrl as string); |
|
|
|
const fileName = fullUrl.pathname.split("/").pop() as string; |
|
|
|
(this.$refs.photoDialog as PhotoDialog).open( |
|
|
|
this.imageCallback, |
|
|
|
this.claimType, |
|
|
|
this.crop, |
|
|
|
urlBlobResponse.data as Blob, |
|
|
|
fileName, |
|
|
|
); |
|
|
|
this.blob = urlBlobResponse.data as Blob; |
|
|
|
this.fileName = fileName; |
|
|
|
this.showRetry = false; |
|
|
|
} catch (error) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
@ -161,11 +314,215 @@ export default class ImageMethodDialog extends Vue { |
|
|
|
} |
|
|
|
} else { |
|
|
|
this.imageCallback(this.imageUrl); |
|
|
|
this.close(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
close() { |
|
|
|
this.visible = false; |
|
|
|
this.stopCameraPreview(); |
|
|
|
const bottomNav = document.querySelector("#QuickNav") as HTMLElement; |
|
|
|
if (bottomNav) { |
|
|
|
bottomNav.style.display = ""; |
|
|
|
} |
|
|
|
this.blob = undefined; |
|
|
|
this.showCameraPreview = false; |
|
|
|
} |
|
|
|
|
|
|
|
async startCameraPreview() { |
|
|
|
logger.debug("startCameraPreview called"); |
|
|
|
logger.debug("Current showCameraPreview state:", this.showCameraPreview); |
|
|
|
logger.debug("Platform capabilities:", this.platformCapabilities); |
|
|
|
|
|
|
|
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; |
|
|
|
} |
|
|
|
|
|
|
|
logger.debug("Starting camera preview for desktop browser"); |
|
|
|
try { |
|
|
|
this.showCameraPreview = true; |
|
|
|
await this.$nextTick(); |
|
|
|
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
|
|
video: { facingMode: "environment" }, |
|
|
|
}); |
|
|
|
this.cameraStream = stream; |
|
|
|
|
|
|
|
await this.$nextTick(); |
|
|
|
|
|
|
|
const videoElement = this.$refs.videoElement as HTMLVideoElement; |
|
|
|
if (videoElement) { |
|
|
|
videoElement.srcObject = stream; |
|
|
|
await new Promise((resolve) => { |
|
|
|
videoElement.onloadedmetadata = () => { |
|
|
|
videoElement.play().then(() => { |
|
|
|
resolve(true); |
|
|
|
}); |
|
|
|
}; |
|
|
|
}); |
|
|
|
} |
|
|
|
} 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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
stopCameraPreview() { |
|
|
|
if (this.cameraStream) { |
|
|
|
this.cameraStream.getTracks().forEach((track) => track.stop()); |
|
|
|
this.cameraStream = null; |
|
|
|
} |
|
|
|
this.showCameraPreview = false; |
|
|
|
} |
|
|
|
|
|
|
|
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.showRetry = true; |
|
|
|
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, |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private createBlobURL(blob: Blob): string { |
|
|
|
return URL.createObjectURL(blob); |
|
|
|
} |
|
|
|
|
|
|
|
async retryImage() { |
|
|
|
this.blob = undefined; |
|
|
|
if (!this.platformCapabilities.isMobile) { |
|
|
|
await this.startCameraPreview(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async uploadImage() { |
|
|
|
this.uploading = true; |
|
|
|
|
|
|
|
if (this.crop) { |
|
|
|
this.blob = (await cropper?.getBlob()) || undefined; |
|
|
|
} |
|
|
|
|
|
|
|
const token = await accessToken(this.activeDid); |
|
|
|
const headers = { |
|
|
|
Authorization: "Bearer " + token, |
|
|
|
}; |
|
|
|
const formData = new FormData(); |
|
|
|
if (!this.blob) { |
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Error", |
|
|
|
text: "There was an error finding the picture. Please try again.", |
|
|
|
}, |
|
|
|
5000, |
|
|
|
); |
|
|
|
this.uploading = false; |
|
|
|
return; |
|
|
|
} |
|
|
|
formData.append("image", this.blob, this.fileName || "photo.jpg"); |
|
|
|
formData.append("claimType", this.claimType); |
|
|
|
try { |
|
|
|
if ( |
|
|
|
window.location.hostname === "localhost" && |
|
|
|
!DEFAULT_IMAGE_API_SERVER.includes("localhost") |
|
|
|
) { |
|
|
|
logger.log( |
|
|
|
"Using shared image API server, so only users on that server can play with images.", |
|
|
|
); |
|
|
|
} |
|
|
|
const response = await axios.post( |
|
|
|
DEFAULT_IMAGE_API_SERVER + "/image", |
|
|
|
formData, |
|
|
|
{ headers }, |
|
|
|
); |
|
|
|
this.uploading = false; |
|
|
|
|
|
|
|
this.close(); |
|
|
|
this.imageCallback(response.data.url as string); |
|
|
|
} catch (error: unknown) { |
|
|
|
let errorMessage = "There was an error saving the picture."; |
|
|
|
|
|
|
|
if (axios.isAxiosError(error)) { |
|
|
|
const status = error.response?.status; |
|
|
|
const data = error.response?.data; |
|
|
|
|
|
|
|
if (status === 401) { |
|
|
|
errorMessage = "Authentication failed. Please try logging in again."; |
|
|
|
} else if (status === 413) { |
|
|
|
errorMessage = "Image file is too large. Please try a smaller image."; |
|
|
|
} else if (status === 415) { |
|
|
|
errorMessage = |
|
|
|
"Unsupported image format. Please try a different image."; |
|
|
|
} else if (status && status >= 500) { |
|
|
|
errorMessage = "Server error. Please try again later."; |
|
|
|
} else if (data?.message) { |
|
|
|
errorMessage = data.message; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.$notify( |
|
|
|
{ |
|
|
|
group: "alert", |
|
|
|
type: "danger", |
|
|
|
title: "Error", |
|
|
|
text: errorMessage, |
|
|
|
}, |
|
|
|
5000, |
|
|
|
); |
|
|
|
this.uploading = false; |
|
|
|
this.blob = undefined; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
@ -191,5 +548,9 @@ export default class ImageMethodDialog extends Vue { |
|
|
|
border-radius: 0.5rem; |
|
|
|
width: 100%; |
|
|
|
max-width: 700px; |
|
|
|
max-height: 90vh; |
|
|
|
overflow: hidden; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
} |
|
|
|
</style> |
|
|
|