|
|
@@ -6,7 +6,7 @@ Assistant de création d'image
|
|
|
<template>
|
|
|
<div class="input-image" >
|
|
|
<UiImage
|
|
|
- ref="imageElement"
|
|
|
+ ref="uiImage"
|
|
|
:image-id="modelValue"
|
|
|
:default-image="defaultImage"
|
|
|
:width="width"
|
|
|
@@ -39,14 +39,14 @@ Assistant de création d'image
|
|
|
ref="cropper"
|
|
|
class="upload__cropper"
|
|
|
check-orientation
|
|
|
- :src="uploadedImage.src"
|
|
|
+ :src="currentImage.src"
|
|
|
:default-position="defaultPosition"
|
|
|
:default-size="defaultSize"
|
|
|
@change="onCropperChange"
|
|
|
/>
|
|
|
|
|
|
<div
|
|
|
- v-if="uploadedImage.src"
|
|
|
+ v-if="currentImage.src"
|
|
|
class="upload__reset-button"
|
|
|
title="Reset Image"
|
|
|
@click="reset()"
|
|
|
@@ -54,12 +54,14 @@ Assistant de création d'image
|
|
|
<v-icon>fas fa-trash</v-icon>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
<div class="upload__buttons-wrapper">
|
|
|
<button class="upload__button" @click="fileInput?.click()">
|
|
|
<input ref="fileInput" type="file" accept="image/*" @change="uploadImage($event)" />
|
|
|
{{$t('upload_image')}}
|
|
|
</button>
|
|
|
</div>
|
|
|
+
|
|
|
<span class="max-size-label">{{ $t('max_size_4_mb') }}</span>
|
|
|
</div>
|
|
|
|
|
|
@@ -78,15 +80,14 @@ Assistant de création d'image
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-
|
|
|
+import { Cropper } from 'vue-advanced-cropper';
|
|
|
+import 'vue-advanced-cropper/dist/style.css';
|
|
|
import {type Ref, ref} from "@vue/reactivity";
|
|
|
import File from '~/models/Core/File'
|
|
|
import type {PropType} from "@vue/runtime-core";
|
|
|
import {useEntityManager} from "~/composables/data/useEntityManager";
|
|
|
import {useImageManager} from "~/composables/data/useImageManager";
|
|
|
-import type { Cropper } from 'vue-advanced-cropper'
|
|
|
-import 'vue-advanced-cropper/dist/style.css';
|
|
|
-import {FILE_FOLDER, FILE_STATUS, FILE_TYPE, FILE_VISIBILITY, TYPE_ALERT} from "~/types/enum/enums";
|
|
|
+import {FILE_VISIBILITY, TYPE_ALERT} from "~/types/enum/enums";
|
|
|
import {usePageStore} from "~/stores/page";
|
|
|
import ImageUtils from "~/services/utils/imageUtils";
|
|
|
|
|
|
@@ -142,24 +143,14 @@ const { em } = useEntityManager()
|
|
|
const { imageManager } = useImageManager()
|
|
|
const pageStore = usePageStore()
|
|
|
|
|
|
-const emit = defineEmits(['update:modelValue', 'update:image', 'reset'])
|
|
|
+const emit = defineEmits(['update:modelValue'])
|
|
|
|
|
|
/**
|
|
|
* Références à des composants
|
|
|
*/
|
|
|
const fileInput: Ref<null | any> = ref(null)
|
|
|
const cropper: Ref<any> = ref(null)
|
|
|
-const imageElement: Ref<any> = ref(null)
|
|
|
-
|
|
|
-/**
|
|
|
- * L'objet File contenant les informations de l'image
|
|
|
- */
|
|
|
-const file: Ref<File | null> = ref(null)
|
|
|
-
|
|
|
-/**
|
|
|
- * L'image elle-même
|
|
|
- */
|
|
|
-const image: Ref<ArrayBuffer | string | null> = ref(null)
|
|
|
+const uiImage: Ref<any> = ref(null)
|
|
|
|
|
|
/**
|
|
|
* L'objet File ou l'image sont en cours de chargement
|
|
|
@@ -171,10 +162,15 @@ const pending: Ref<boolean> = ref(false)
|
|
|
*/
|
|
|
const showModal = ref(false)
|
|
|
|
|
|
+/**
|
|
|
+ * L'objet File contenant les informations de l'image
|
|
|
+ */
|
|
|
+const file: Ref<File | null> = ref(null)
|
|
|
+
|
|
|
/**
|
|
|
* Données d'une nouvelle image uploadée par l'utilisateur
|
|
|
*/
|
|
|
-const uploadedImage: Ref<
|
|
|
+const currentImage: Ref<
|
|
|
{id: string | number | null, src: string | null, content: string | null, name: string | null}
|
|
|
> = ref({
|
|
|
id: null,
|
|
|
@@ -183,18 +179,21 @@ const uploadedImage: Ref<
|
|
|
name: null
|
|
|
})
|
|
|
|
|
|
+/**
|
|
|
+ * Taille maximale autorisée pour les images uploadées (en bytes)
|
|
|
+ */
|
|
|
const MAX_FILE_SIZE = 4 * 1024 * 1024
|
|
|
|
|
|
/**
|
|
|
* Coordonnées du cropper
|
|
|
*/
|
|
|
-const coordinates: Ref<{ left?: number, top?: number, height?: number, width?: number }> = ref({})
|
|
|
+const cropperConfig: Ref<{ left?: number, top?: number, height?: number, width?: number }> = ref({})
|
|
|
|
|
|
/**
|
|
|
* @see https://advanced-cropper.github.io/vue-advanced-cropper/components/cropper.html#defaultposition
|
|
|
*/
|
|
|
const defaultPosition = () => {
|
|
|
- return { left : coordinates.value.left, top : coordinates.value.top }
|
|
|
+ return { left : cropperConfig.value.left, top : cropperConfig.value.top }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -207,11 +206,32 @@ const defaultSize = (params: any): { width: number, height: number } | null => {
|
|
|
const { imageSize, visibleArea } = params
|
|
|
|
|
|
return {
|
|
|
- width: coordinates.value.width ?? (visibleArea || imageSize).width,
|
|
|
- height: coordinates.value.height ?? (visibleArea || imageSize).height
|
|
|
+ width: cropperConfig.value.width ?? (visibleArea || imageSize).width,
|
|
|
+ height: cropperConfig.value.height ?? (visibleArea || imageSize).height
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Charge l'image correspondant au fichier dans le cropper
|
|
|
+ *
|
|
|
+ * @param fileId
|
|
|
+ */
|
|
|
+const loadImage = async (fileId: number) => {
|
|
|
+ file.value = await em.fetch(File, fileId) as File
|
|
|
+
|
|
|
+ if (file.value.config) {
|
|
|
+ const fileConfig = JSON.parse(file.value.config)
|
|
|
+ cropperConfig.value.left = fileConfig.x
|
|
|
+ cropperConfig.value.top = fileConfig.y
|
|
|
+ cropperConfig.value.height = fileConfig.height
|
|
|
+ cropperConfig.value.width = fileConfig.width
|
|
|
+ }
|
|
|
+
|
|
|
+ currentImage.value.name = file.value.name
|
|
|
+ currentImage.value.id = file.value.id
|
|
|
+ currentImage.value.src = await imageManager.get(fileId) as string
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Affiche la modale d'upload / modification de l'image
|
|
|
*/
|
|
|
@@ -221,20 +241,7 @@ const openModal = async () => {
|
|
|
|
|
|
if (props.modelValue !== null) {
|
|
|
// Un objet File existe déjà: on le récupère
|
|
|
- file.value = await em.fetch(File, props.modelValue) as File
|
|
|
- image.value = await imageManager.get(props.modelValue)
|
|
|
-
|
|
|
- if (file.value.config) {
|
|
|
- const config = JSON.parse(file.value.config)
|
|
|
- coordinates.value.left = config.x
|
|
|
- coordinates.value.top = config.y
|
|
|
- coordinates.value.height = config.height
|
|
|
- coordinates.value.width = config.width
|
|
|
- }
|
|
|
-
|
|
|
- uploadedImage.value.name = file.value.name
|
|
|
- uploadedImage.value.id = file.value.id
|
|
|
- uploadedImage.value.src = image.value as string
|
|
|
+ await loadImage(props.modelValue)
|
|
|
|
|
|
} else {
|
|
|
// Nouveau File
|
|
|
@@ -245,16 +252,20 @@ const openModal = async () => {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Réinitialise l'image sélectionnée
|
|
|
+ * Réinitialise l'image actuellement chargée dans le cropper
|
|
|
*/
|
|
|
const reset = () => {
|
|
|
- if (uploadedImage.value.src !== null) {
|
|
|
- URL.revokeObjectURL(uploadedImage.value.src)
|
|
|
+ if (currentImage.value.src !== null) {
|
|
|
+ URL.revokeObjectURL(currentImage.value.src)
|
|
|
+ }
|
|
|
+
|
|
|
+ currentImage.value = {
|
|
|
+ src: null, content: null, name: null, id: null
|
|
|
+ }
|
|
|
+
|
|
|
+ cropperConfig.value = {
|
|
|
+ left: undefined, height: undefined, top: undefined, width: undefined
|
|
|
}
|
|
|
- uploadedImage.value.src = null
|
|
|
- uploadedImage.value.content = null
|
|
|
- uploadedImage.value.name = null
|
|
|
- uploadedImage.value.id = null
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -275,16 +286,17 @@ const uploadImage = async (event: any) => {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- reset()
|
|
|
-
|
|
|
- uploadedImage.value.name = uploadedFile.name
|
|
|
- uploadedImage.value.src = URL.createObjectURL(uploadedFile)
|
|
|
- uploadedImage.value.content = await ImageUtils.blobToBase64(uploadedFile)
|
|
|
-
|
|
|
- coordinates.value.top = 0
|
|
|
- coordinates.value.left = 0
|
|
|
- coordinates.value.height = uploadedFile.height
|
|
|
- coordinates.value.width = uploadedFile.width
|
|
|
+ // Met à jour l'image dans le cropper
|
|
|
+ currentImage.value.id = null
|
|
|
+ currentImage.value.name = uploadedFile.name
|
|
|
+ currentImage.value.src = URL.createObjectURL(uploadedFile)
|
|
|
+ currentImage.value.content = await ImageUtils.blobToBase64(uploadedFile)
|
|
|
+
|
|
|
+ // Met à jour la configuration du cropper
|
|
|
+ cropperConfig.value.top = 0
|
|
|
+ cropperConfig.value.left = 0
|
|
|
+ cropperConfig.value.height = uploadedFile.height
|
|
|
+ cropperConfig.value.width = uploadedFile.width
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -292,7 +304,7 @@ const uploadImage = async (event: any) => {
|
|
|
* @param newCoordinates
|
|
|
*/
|
|
|
const onCropperChange = ({ coordinates: newCoordinates } : any) => {
|
|
|
- coordinates.value = newCoordinates;
|
|
|
+ cropperConfig.value = newCoordinates;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -304,47 +316,34 @@ const cancel = () => {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Construit la nouvelle config du fichier à partir des réglages actuels
|
|
|
- */
|
|
|
-const updateFileConfig = () => {
|
|
|
- file.value!!.config = JSON.stringify({
|
|
|
- x: coordinates.value.left,
|
|
|
- y: coordinates.value.top,
|
|
|
- height: coordinates.value.height,
|
|
|
- width: coordinates.value.width
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * Enregistre une nouvelle image
|
|
|
+ * Enregistre une nouvelle image et retourne l'id du fichier nouvellement créé
|
|
|
*/
|
|
|
-const saveNewImage = async () => {
|
|
|
+const saveNewImage = async (): Promise<number> => {
|
|
|
if (!file.value) {
|
|
|
throw new Error('No File object defined')
|
|
|
}
|
|
|
- if (!uploadedImage.value.name) {
|
|
|
+ if (!currentImage.value.name) {
|
|
|
throw new Error("Missing file's name")
|
|
|
}
|
|
|
- if (!uploadedImage.value.content) {
|
|
|
+ if (!currentImage.value.content) {
|
|
|
throw new Error("Missing file's content")
|
|
|
}
|
|
|
|
|
|
const config = JSON.stringify({
|
|
|
- x: coordinates.value.left,
|
|
|
- y: coordinates.value.top,
|
|
|
- height: coordinates.value.height,
|
|
|
- width: coordinates.value.width
|
|
|
+ x: cropperConfig.value.left,
|
|
|
+ y: cropperConfig.value.top,
|
|
|
+ height: cropperConfig.value.height,
|
|
|
+ width: cropperConfig.value.width
|
|
|
})
|
|
|
|
|
|
const response = await imageManager.upload(
|
|
|
- uploadedImage.value.name,
|
|
|
- uploadedImage.value.content,
|
|
|
+ currentImage.value.name,
|
|
|
+ currentImage.value.content,
|
|
|
FILE_VISIBILITY.EVERYBODY,
|
|
|
config
|
|
|
) as any
|
|
|
|
|
|
- //On émet un évent afin de mettre à jour le formulaire de départ
|
|
|
- emit('update:modelValue', response.fileId)
|
|
|
+ return response.fileId
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -355,9 +354,14 @@ const saveExistingImage = async () => {
|
|
|
throw new Error('No File object defined')
|
|
|
}
|
|
|
|
|
|
- updateFileConfig()
|
|
|
+ file.value.config = JSON.stringify({
|
|
|
+ x: cropperConfig.value.left,
|
|
|
+ y: cropperConfig.value.top,
|
|
|
+ height: cropperConfig.value.height,
|
|
|
+ width: cropperConfig.value.width
|
|
|
+ })
|
|
|
|
|
|
- await em.persist(File, file.value) // TODO: à revoir
|
|
|
+ await em.persist(File, file.value)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -368,21 +372,24 @@ const save = async () => {
|
|
|
if (!file.value) {
|
|
|
throw new Error('No File object defined')
|
|
|
}
|
|
|
+
|
|
|
pageStore.loading = true
|
|
|
|
|
|
- if (uploadedImage.value.src && uploadedImage.value.src !== image.value) {
|
|
|
+ if (currentImage.value.src && currentImage.value.id === null) {
|
|
|
// Une nouvelle image a été uploadée
|
|
|
- await saveNewImage()
|
|
|
- } else if (uploadedImage.value.id) {
|
|
|
+ const fileId = await saveNewImage()
|
|
|
+ emit('update:modelValue', fileId)
|
|
|
+
|
|
|
+ } else if (currentImage.value.id) {
|
|
|
// L'image existante a été modifiée
|
|
|
await saveExistingImage()
|
|
|
+ uiImage.value.refresh()
|
|
|
+
|
|
|
} else {
|
|
|
// On a reset l'image
|
|
|
emit('update:modelValue', null)
|
|
|
}
|
|
|
|
|
|
- imageElement.value.refresh()
|
|
|
-
|
|
|
showModal.value = false
|
|
|
pageStore.loading = false
|
|
|
}
|
|
|
@@ -391,8 +398,8 @@ const save = async () => {
|
|
|
* Lorsqu'on démonte le component on supprime le watcher et on revoke l'objet URL
|
|
|
*/
|
|
|
onUnmounted(() => {
|
|
|
- if (uploadedImage.value && uploadedImage.value.src) {
|
|
|
- URL.revokeObjectURL(uploadedImage.value.src)
|
|
|
+ if (currentImage.value && currentImage.value.src) {
|
|
|
+ URL.revokeObjectURL(currentImage.value.src)
|
|
|
}
|
|
|
})
|
|
|
</script>
|