|
|
@@ -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,18 +80,16 @@ Assistant de création d'image
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-
|
|
|
-import {ref} from "@vue/reactivity";
|
|
|
+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 type {AnyJson} from "~/types/data";
|
|
|
-import ApiResource from "~/models/ApiResource";
|
|
|
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";
|
|
|
|
|
|
const props = defineProps({
|
|
|
/**
|
|
|
@@ -143,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<ApiResource | 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
|
|
|
@@ -172,28 +162,38 @@ 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<AnyJson> = ref({
|
|
|
+const currentImage: Ref<
|
|
|
+ {id: string | number | null, src: string | null, content: string | null, name: string | null}
|
|
|
+> = ref({
|
|
|
id: null,
|
|
|
src: null,
|
|
|
- file: null,
|
|
|
+ content: null,
|
|
|
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 }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -206,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
|
|
|
*/
|
|
|
@@ -220,45 +241,38 @@ 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)
|
|
|
- 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
|
|
|
+ await loadImage(props.modelValue)
|
|
|
|
|
|
} else {
|
|
|
// Nouveau File
|
|
|
- file.value = em.newInstance(File)
|
|
|
+ file.value = em.newInstance(File) as File
|
|
|
}
|
|
|
|
|
|
pending.value = false
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Réinitialise l'image sélectionnée
|
|
|
+ * Réinitialise l'image actuellement chargée dans le cropper
|
|
|
*/
|
|
|
const reset = () => {
|
|
|
- uploadedImage.value.src = null
|
|
|
- uploadedImage.value.file = null
|
|
|
- uploadedImage.value.name = null
|
|
|
- uploadedImage.value.id = 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
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Upload une image depuis le poste client
|
|
|
* @param event
|
|
|
*/
|
|
|
-const uploadImage = (event: any) => {
|
|
|
+const uploadImage = async (event: any) => {
|
|
|
const { files } = event.target
|
|
|
|
|
|
if (!files || !files[0]) {
|
|
|
@@ -272,16 +286,17 @@ const uploadImage = (event: any) => {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- reset()
|
|
|
-
|
|
|
- uploadedImage.value.name = files[0].name
|
|
|
- uploadedImage.value.src = URL.createObjectURL(files[0])
|
|
|
- uploadedImage.value.file = files[0]
|
|
|
-
|
|
|
- coordinates.value.top = 0
|
|
|
- coordinates.value.left = 0
|
|
|
- coordinates.value.height = uploadedImage.value.height
|
|
|
- coordinates.value.width = uploadedImage.value.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
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -289,7 +304,7 @@ const uploadImage = (event: any) => {
|
|
|
* @param newCoordinates
|
|
|
*/
|
|
|
const onCropperChange = ({ coordinates: newCoordinates } : any) => {
|
|
|
- coordinates.value = newCoordinates;
|
|
|
+ cropperConfig.value = newCoordinates;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -301,46 +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')
|
|
|
}
|
|
|
-
|
|
|
- // On créé l'objet File à sauvegarder
|
|
|
- file.value.name = uploadedImage.value.name
|
|
|
- file.value.imgFieldName = props.field
|
|
|
- file.value.visibility = FILE_VISIBILITY.EVERYBODY
|
|
|
- file.value.folder = FILE_FOLDER.IMAGES
|
|
|
- file.value.status = FILE_STATUS.READY
|
|
|
- file.value.type = FILE_TYPE.UPLOADED
|
|
|
-
|
|
|
- updateFileConfig()
|
|
|
-
|
|
|
- if (props.ownerId) {
|
|
|
- // TODO: revoir
|
|
|
- file.value.ownerId = props.ownerId
|
|
|
+ if (!currentImage.value.name) {
|
|
|
+ throw new Error("Missing file's name")
|
|
|
+ }
|
|
|
+ if (!currentImage.value.content) {
|
|
|
+ throw new Error("Missing file's content")
|
|
|
}
|
|
|
|
|
|
- // TODO: A revoir, on doit pouvoir persister l'image aussi
|
|
|
- const returnedFile = await em.persist(File, file.value)
|
|
|
- // await imageManager.persist(file.value, uploadedImage.src)
|
|
|
+ const config = JSON.stringify({
|
|
|
+ x: cropperConfig.value.left,
|
|
|
+ y: cropperConfig.value.top,
|
|
|
+ height: cropperConfig.value.height,
|
|
|
+ width: cropperConfig.value.width
|
|
|
+ })
|
|
|
+
|
|
|
+ const response = await imageManager.upload(
|
|
|
+ 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', returnedFile.id)
|
|
|
+ return response.fileId
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -351,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)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -361,24 +369,23 @@ const saveExistingImage = async () => {
|
|
|
*/
|
|
|
// TODO: Voir si tout ou partie de cette fonction peut passer dans le useImageFetch, imageManager ou imageUtils
|
|
|
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
|
|
|
}
|
|
|
@@ -387,8 +394,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>
|