You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
313 lines
8.4 KiB
313 lines
8.4 KiB
<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
|
|
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"
|
|
>
|
|
<span v-if="uploading"> Uploading... </span>
|
|
<span v-else-if="blob"> Look Good? </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"
|
|
@click="close()"
|
|
>
|
|
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
|
</div>
|
|
</div>
|
|
|
|
<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-if="blob">
|
|
<div v-if="crop">
|
|
<VuePictureCropper
|
|
:box-style="{
|
|
backgroundColor: '#f8f8f8',
|
|
margin: 'auto',
|
|
}"
|
|
:img="createBlobURL(blob)"
|
|
:options="{
|
|
viewMode: 1,
|
|
dragMode: 'crop',
|
|
aspectRatio: 9 / 9,
|
|
}"
|
|
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="absolute bottom-[1rem] left-[1rem] px-2 py-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-1 px-2 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"
|
|
@click="retryImage"
|
|
>
|
|
<span>Retry</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<div class="flex flex-col items-center justify-center gap-4 p-4">
|
|
<button
|
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
|
@click="takePhoto"
|
|
>
|
|
<font-awesome icon="camera" class="w-[1em]" />
|
|
</button>
|
|
<button
|
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
|
@click="pickPhoto"
|
|
>
|
|
<font-awesome icon="image" class="w-[1em]" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
/**
|
|
* PhotoDialog.vue - Cross-platform photo capture and selection component
|
|
*
|
|
* This component provides a unified interface for taking photos and selecting images
|
|
* across different platforms using the PlatformService.
|
|
*
|
|
* @author Matthew Raymer
|
|
* @file PhotoDialog.vue
|
|
*/
|
|
|
|
import axios from "axios";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
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";
|
|
|
|
@Component({ components: { VuePictureCropper } })
|
|
export default class PhotoDialog extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
activeDid = "";
|
|
blob?: Blob;
|
|
claimType = "";
|
|
crop = false;
|
|
fileName?: string;
|
|
setImageCallback: (arg: string) => void = () => {};
|
|
showRetry = true;
|
|
uploading = false;
|
|
visible = false;
|
|
|
|
private platformService = PlatformServiceFactory.getInstance();
|
|
URL = window.URL || window.webkitURL;
|
|
|
|
async mounted() {
|
|
try {
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.activeDid = settings.activeDid || "";
|
|
} catch (err: any) {
|
|
logger.error("Error retrieving settings from database:", err);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: err.message || "There was an error retrieving your settings.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
}
|
|
|
|
open(
|
|
setImageFn: (arg: string) => void,
|
|
claimType: string,
|
|
crop?: boolean,
|
|
blob?: Blob,
|
|
inputFileName?: string,
|
|
) {
|
|
this.visible = true;
|
|
this.claimType = claimType;
|
|
this.crop = !!crop;
|
|
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
|
if (bottomNav) {
|
|
bottomNav.style.display = "none";
|
|
}
|
|
this.setImageCallback = setImageFn;
|
|
if (blob) {
|
|
this.blob = blob;
|
|
this.fileName = inputFileName;
|
|
this.showRetry = false;
|
|
} else {
|
|
this.blob = undefined;
|
|
this.fileName = undefined;
|
|
this.showRetry = true;
|
|
}
|
|
}
|
|
|
|
close() {
|
|
this.visible = false;
|
|
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
|
if (bottomNav) {
|
|
bottomNav.style.display = "";
|
|
}
|
|
this.blob = undefined;
|
|
}
|
|
|
|
async takePhoto() {
|
|
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,
|
|
);
|
|
}
|
|
}
|
|
|
|
async pickPhoto() {
|
|
try {
|
|
const result = await this.platformService.pickImage();
|
|
this.blob = result.blob;
|
|
this.fileName = result.fileName;
|
|
} catch (error) {
|
|
logger.error("Error picking image:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Failed to pick image. Please try again.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
private createBlobURL(blob: Blob): string {
|
|
return URL.createObjectURL(blob);
|
|
}
|
|
|
|
async retryImage() {
|
|
this.blob = undefined;
|
|
}
|
|
|
|
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.setImageCallback(response.data.url as string);
|
|
} catch (error) {
|
|
logger.error("Error uploading the image", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "There was an error saving the picture.",
|
|
},
|
|
5000,
|
|
);
|
|
this.uploading = false;
|
|
this.blob = undefined;
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.dialog-overlay {
|
|
z-index: 60;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.dialog {
|
|
background-color: white;
|
|
padding: 1rem;
|
|
border-radius: 0.5rem;
|
|
width: 100%;
|
|
max-width: 700px;
|
|
}
|
|
</style>
|
|
|