Browse Source

refresh UiImage on image upload

Olivier Massot 2 years ago
parent
commit
04e48e57fb
3 changed files with 118 additions and 106 deletions
  1. 13 11
      components/Ui/Image.vue
  2. 96 89
      components/Ui/Input/Image.vue
  3. 9 6
      composables/data/useImageFetch.ts

+ 13 - 11
components/Ui/Image.vue

@@ -6,7 +6,7 @@ Permet d'afficher une image par défaut si l'image demandée n'est pas disponibl
   <main>
     <div class="image-wrapper" :style="{width: width + 'px'}">
       <v-img
-        :src="imageSrc"
+        :src="imageSrc ?? undefined"
         :lazy-src="defaultImagePath"
         :height="height"
         :width="width"
@@ -37,6 +37,8 @@ Permet d'afficher une image par défaut si l'image demandée n'est pas disponibl
 <script setup lang="ts">
 import {useImageFetch} from "~/composables/data/useImageFetch";
 import ImageManager from "~/services/data/imageManager";
+import type {WatchStopHandle} from "@vue/runtime-core";
+import type {Ref} from "@vue/reactivity";
 
 const props = defineProps({
   /**
@@ -82,24 +84,24 @@ const { fetch } = useImageFetch()
 
 const defaultImagePath = props.defaultImage ?? ImageManager.defaultImage
 
-const { data: imageSrc, pending, refresh: refreshImage } = fetch(props.imageId ?? null, defaultImagePath, props.height, props.width) as any
-
-
 const emit = defineEmits(['overlay-clicked'])
 
-/**
- * Si l'id change, on recharge l'image
- */
-const unwatch: WatchStopHandle = watch(() => props.imageId, async () => {
-  await refreshImage()
-})
+const fileId = toRef(props, 'imageId')
+
+const { data: imageSrc, pending, refresh: refreshImage } = await fetch(fileId, defaultImagePath, props.height, props.width) as any
 
 const refresh = () => {
   refreshImage()
 }
-
 defineExpose({ refresh })
 
+/**
+ * Si l'id change, on recharge l'image
+ */
+const unwatch: WatchStopHandle = watch(() => props.imageId, async (value, oldValue) => {
+  refresh()
+})
+
 /**
  * Lorsqu'on démonte le component, on supprime le watcher
  */

+ 96 - 89
components/Ui/Input/Image.vue

@@ -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>

+ 9 - 6
composables/data/useImageFetch.ts

@@ -1,8 +1,10 @@
 import {useImageManager} from "~/composables/data/useImageManager";
-import type {FetchResult} from "#app";
+import type {AsyncData, FetchResult} from "#app";
+import {v4 as uuid4} from "uuid";
+import type {Ref} from "@vue/reactivity";
 
 interface useImageFetchReturnType {
-    fetch: (id: number | null, defaultImage?: string | null, height?: number, width?: number) => AsyncData<string | ArrayBuffer | null, Error | null>
+    fetch: (id: Ref<number | null>, defaultImage?: string | null, height?: number, width?: number) => AsyncData<string | ArrayBuffer | null, Error | null>
 }
 
 /**
@@ -12,14 +14,15 @@ export const useImageFetch = (): useImageFetchReturnType => {
     const { imageManager } = useImageManager()
 
     const fetch = (
-        id: number | null,  // If id is null, fetch shall return the default image url
+        id: Ref<number | null>,  // If id is null, fetch shall return the default image url
         defaultImage: string | null = null,
         height: number = 0,
         width: number = 0
     ) => useAsyncData(
-        'img' + (id ?? defaultImage ?? 0),
-        () => imageManager.get(id, defaultImage, height, width),
-        { lazy: true, server: false }  // Always fetch images client-side
+        'img' + (id ?? defaultImage ?? 0) + '_' + uuid4(),
+        () => imageManager.get(id.value, defaultImage, height, width),
+        { lazy: true, server: false },  // Always fetch images client-side
+
     )
 
     return { fetch }