mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 13:23:14 +01:00
feat: proof of concept
This commit is contained in:
@@ -1,3 +1,90 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { Cropper } from "vue-advanced-cropper";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import MdiDelete from "~icons/mdi/delete";
|
||||||
|
import MdiRotateLeft from "~icons/mdi/rotate-left";
|
||||||
|
import MdiRotateRight from "~icons/mdi/rotate-right";
|
||||||
|
import MdiFlipHorizontal from "~icons/mdi/flip-horizontal";
|
||||||
|
import MdiFlipVertical from "~icons/mdi/flip-vertical";
|
||||||
|
// import MdiStarOutline from "~icons/mdi/star-outline";
|
||||||
|
// import MdiStar from "~icons/mdi/star";
|
||||||
|
|
||||||
|
import "vue-advanced-cropper/dist/style.css";
|
||||||
|
|
||||||
|
export type PhotoPreview = {
|
||||||
|
photoName: string;
|
||||||
|
file: File;
|
||||||
|
fileBase64: string;
|
||||||
|
primary: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<{ initialPhotos: PhotoPreview[] }>();
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: "update:photos", photos: PhotoPreview[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const photos = ref<PhotoPreview[]>(props.initialPhotos);
|
||||||
|
const croppers = ref<(InstanceType<typeof Cropper> | null)[]>([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
croppers.value = Array(photos.value.length).fill(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleFileChange(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
if (input.files && input.files.length > 0) {
|
||||||
|
for (const file of input.files) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
const photo = {
|
||||||
|
photoName: file.name,
|
||||||
|
fileBase64: e.target?.result as string,
|
||||||
|
file,
|
||||||
|
primary: photos.value.length === 0,
|
||||||
|
};
|
||||||
|
photos.value.push(photo);
|
||||||
|
emits("update:photos", photos.value);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
input.value = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteImage(index: number) {
|
||||||
|
photos.value.splice(index, 1);
|
||||||
|
croppers.value.splice(index, 1);
|
||||||
|
emits("update:photos", photos.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// function setPrimary(index: number) {
|
||||||
|
// const primary = photos.value.findIndex(p => p.primary);
|
||||||
|
|
||||||
|
// if (primary !== -1) photos.value[primary].primary = false;
|
||||||
|
// if (primary !== index) photos.value[index].primary = true;
|
||||||
|
|
||||||
|
// toast.error("Currently this does not do anything, the first photo will always be primary");
|
||||||
|
// }
|
||||||
|
|
||||||
|
const setSize = (index: number) => {
|
||||||
|
const cropper = croppers.value[index];
|
||||||
|
const img = new Image();
|
||||||
|
img.src = photos.value[index].fileBase64;
|
||||||
|
img.onload = () => {
|
||||||
|
// get the image size
|
||||||
|
cropper?.setCoordinates({
|
||||||
|
width: img.naturalWidth,
|
||||||
|
height: img.naturalHeight,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex w-full flex-col gap-1.5">
|
<div class="flex w-full flex-col gap-1.5">
|
||||||
@@ -18,17 +105,47 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="photos.length > 0" class="mt-4 border-t border-gray-300 px-4 pb-4">
|
<div v-if="photos.length > 0" class="mt-4 border-t border-gray-300">
|
||||||
<div v-for="(photo, index) in photos" :key="index">
|
<div v-for="(photo, index) in photos" :key="index">
|
||||||
<div class="mt-8 w-full">
|
<div class="mt-8 w-full">
|
||||||
<img
|
<cropper
|
||||||
|
ref="croppers"
|
||||||
:src="photo.fileBase64"
|
:src="photo.fileBase64"
|
||||||
class="w-full rounded border-gray-300 object-fill shadow-sm"
|
|
||||||
alt="Uploaded Photo"
|
alt="Uploaded Photo"
|
||||||
|
background-class="image-cropper-bg"
|
||||||
|
class="image-cropper"
|
||||||
|
@ready="
|
||||||
|
() => {
|
||||||
|
setSize(index);
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
|
<!-- class="w-full rounded border-gray-300 object-fill shadow-sm" -->
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex items-center gap-2">
|
<div class="mt-2 flex justify-center gap-2">
|
||||||
<TooltipProvider class="flex gap-2">
|
<TooltipProvider class="flex gap-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button size="icon" type="button" variant="outline" @click.prevent="croppers[index]?.rotate(-90)">
|
||||||
|
<MdiRotateLeft />
|
||||||
|
<div class="sr-only">Rotate left</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Rotate left</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button size="icon" type="button" variant="outline" @click.prevent="croppers[index]?.flip(true, false)">
|
||||||
|
<MdiFlipHorizontal />
|
||||||
|
<div class="sr-only">Flip horizontal</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Flip horizontal</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Button size="icon" type="button" variant="destructive" @click.prevent="deleteImage(index)">
|
<Button size="icon" type="button" variant="destructive" @click.prevent="deleteImage(index)">
|
||||||
@@ -58,69 +175,42 @@
|
|||||||
<p>Set as {{ photo.primary ? "non" : "" }} primary photo</p>
|
<p>Set as {{ photo.primary ? "non" : "" }} primary photo</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip> -->
|
</Tooltip> -->
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button size="icon" type="button" variant="outline" @click.prevent="croppers[index]?.flip(false, true)">
|
||||||
|
<MdiFlipVertical />
|
||||||
|
<div class="sr-only">Flip vertical</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Flip vertical</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button size="icon" type="button" variant="outline" @click.prevent="croppers[index]?.rotate(90)">
|
||||||
|
<MdiRotateRight />
|
||||||
|
<div class="sr-only">Rotate right</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Rotate right</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<p class="mt-1 text-sm" style="overflow-wrap: anywhere">{{ photo.photoName }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-1 text-center text-sm" style="overflow-wrap: anywhere">{{ photo.photoName }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<style>
|
||||||
import { ref } from "vue";
|
.image-cropper {
|
||||||
import { Label } from "@/components/ui/label";
|
width: 462px;
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import MdiDelete from "~icons/mdi/delete";
|
|
||||||
// import MdiStarOutline from "~icons/mdi/star-outline";
|
|
||||||
// import MdiStar from "~icons/mdi/star";
|
|
||||||
|
|
||||||
export type PhotoPreview = {
|
|
||||||
photoName: string;
|
|
||||||
file: File;
|
|
||||||
fileBase64: string;
|
|
||||||
primary: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const props = defineProps<{ initialPhotos: PhotoPreview[] }>();
|
|
||||||
const emits = defineEmits<{
|
|
||||||
(e: "update:photos", photos: PhotoPreview[]): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const photos = ref<PhotoPreview[]>(props.initialPhotos);
|
|
||||||
|
|
||||||
function handleFileChange(event: Event) {
|
|
||||||
const input = event.target as HTMLInputElement;
|
|
||||||
if (input.files && input.files.length > 0) {
|
|
||||||
for (const file of input.files) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = e => {
|
|
||||||
const photo = {
|
|
||||||
photoName: file.name,
|
|
||||||
fileBase64: e.target?.result as string,
|
|
||||||
file,
|
|
||||||
primary: photos.value.length === 0,
|
|
||||||
};
|
|
||||||
photos.value.push(photo);
|
|
||||||
emits("update:photos", photos.value);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
input.value = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteImage(index: number) {
|
.image-cropper-bg {
|
||||||
photos.value.splice(index, 1);
|
background-color: white;
|
||||||
emits("update:photos", photos.value);
|
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
// function setPrimary(index: number) {
|
|
||||||
// const primary = photos.value.findIndex(p => p.primary);
|
|
||||||
|
|
||||||
// if (primary !== -1) photos.value[primary].primary = false;
|
|
||||||
// if (primary !== index) photos.value[index].primary = true;
|
|
||||||
|
|
||||||
// toast.error("Currently this does not do anything, the first photo will always be primary");
|
|
||||||
// }
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul-vue": "^0.4.1",
|
"vaul-vue": "^0.4.1",
|
||||||
"vue": "3.4.8",
|
"vue": "3.4.8",
|
||||||
|
"vue-advanced-cropper": "^2.8.9",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"vue-sonner": "^1.3.0"
|
"vue-sonner": "^1.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
31
frontend/pnpm-lock.yaml
generated
31
frontend/pnpm-lock.yaml
generated
@@ -110,6 +110,9 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: 3.4.8
|
specifier: 3.4.8
|
||||||
version: 3.4.8(typescript@5.6.2)
|
version: 3.4.8(typescript@5.6.2)
|
||||||
|
vue-advanced-cropper:
|
||||||
|
specifier: ^2.8.9
|
||||||
|
version: 2.8.9(vue@3.4.8(typescript@5.6.2))
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0(vue@3.4.8(typescript@5.6.2))
|
version: 4.5.0(vue@3.4.8(typescript@5.6.2))
|
||||||
@@ -2510,6 +2513,9 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
|
classnames@2.5.1:
|
||||||
|
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||||
|
|
||||||
clean-regexp@1.0.0:
|
clean-regexp@1.0.0:
|
||||||
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -2763,6 +2769,9 @@ packages:
|
|||||||
de-indent@1.0.2:
|
de-indent@1.0.2:
|
||||||
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
|
||||||
|
|
||||||
|
debounce@1.2.1:
|
||||||
|
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2926,6 +2935,9 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
easy-bem@1.1.1:
|
||||||
|
resolution: {integrity: sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==}
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@@ -5891,6 +5903,12 @@ packages:
|
|||||||
vscode-uri@3.1.0:
|
vscode-uri@3.1.0:
|
||||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||||
|
|
||||||
|
vue-advanced-cropper@2.8.9:
|
||||||
|
resolution: {integrity: sha512-1jc5gO674kVGpJKekoaol6ZlwaF5VYDLSBwBOUpViW0IOrrRsyLw6XNszjEqgbavvqinlKNS6Kqlom3B5M72Tw==}
|
||||||
|
engines: {node: '>=8', npm: '>=5'}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.0.0
|
||||||
|
|
||||||
vue-bundle-renderer@2.1.1:
|
vue-bundle-renderer@2.1.1:
|
||||||
resolution: {integrity: sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==}
|
resolution: {integrity: sha512-+qALLI5cQncuetYOXp4yScwYvqh8c6SMXee3B+M7oTZxOgtESP0l4j/fXdEJoZ+EdMxkGWIj+aSEyjXkOdmd7g==}
|
||||||
|
|
||||||
@@ -8915,6 +8933,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
|
classnames@2.5.1: {}
|
||||||
|
|
||||||
clean-regexp@1.0.0:
|
clean-regexp@1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 1.0.5
|
escape-string-regexp: 1.0.5
|
||||||
@@ -9157,6 +9177,8 @@ snapshots:
|
|||||||
|
|
||||||
de-indent@1.0.2: {}
|
de-indent@1.0.2: {}
|
||||||
|
|
||||||
|
debounce@1.2.1: {}
|
||||||
|
|
||||||
debug@2.6.9:
|
debug@2.6.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.0.0
|
ms: 2.0.0
|
||||||
@@ -9284,6 +9306,8 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
|
easy-bem@1.1.1: {}
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
ejs@3.1.10:
|
ejs@3.1.10:
|
||||||
@@ -12728,6 +12752,13 @@ snapshots:
|
|||||||
|
|
||||||
vscode-uri@3.1.0: {}
|
vscode-uri@3.1.0: {}
|
||||||
|
|
||||||
|
vue-advanced-cropper@2.8.9(vue@3.4.8(typescript@5.6.2)):
|
||||||
|
dependencies:
|
||||||
|
classnames: 2.5.1
|
||||||
|
debounce: 1.2.1
|
||||||
|
easy-bem: 1.1.1
|
||||||
|
vue: 3.4.8(typescript@5.6.2)
|
||||||
|
|
||||||
vue-bundle-renderer@2.1.1:
|
vue-bundle-renderer@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
ufo: 1.5.4
|
ufo: 1.5.4
|
||||||
|
|||||||
Reference in New Issue
Block a user