Browse Source

feat: support png, gif, webp (#7947)

Co-authored-by: xuanson9699 <84961581+xuanson9699@users.noreply.github.com>
Nam Vu 5 months ago
parent
commit
ac0fed6402

+ 33 - 10
web/app/components/base/app-icon-picker/Uploader.tsx

@@ -8,18 +8,22 @@ import classNames from 'classnames'
 
 import { ImagePlus } from '../icons/src/vender/line/images'
 import { useDraggableUploader } from './hooks'
+import { checkIsAnimatedImage } from './utils'
 import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
 
 type UploaderProps = {
   className?: string
   onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
+  onUpload?: (file?: File) => void
 }
 
 const Uploader: FC<UploaderProps> = ({
   className,
   onImageCropped,
+  onUpload,
 }) => {
   const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
+  const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
   useEffect(() => {
     return () => {
       if (inputImage)
@@ -34,12 +38,19 @@ const Uploader: FC<UploaderProps> = ({
     if (!inputImage)
       return
     onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name)
+    onUpload?.(undefined)
   }
 
   const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0]
-    if (file)
+    if (file) {
       setInputImage({ file, url: URL.createObjectURL(file) })
+      checkIsAnimatedImage(file).then((isAnimatedImage) => {
+        setIsAnimatedImage(!!isAnimatedImage)
+        if (isAnimatedImage)
+          onUpload?.(file)
+      })
+    }
   }
 
   const {
@@ -52,6 +63,26 @@ const Uploader: FC<UploaderProps> = ({
 
   const inputRef = createRef<HTMLInputElement>()
 
+  const handleShowImage = () => {
+    if (isAnimatedImage) {
+      return (
+        <img src={inputImage?.url} alt='' />
+      )
+    }
+
+    return (
+      <Cropper
+        image={inputImage?.url}
+        crop={crop}
+        zoom={zoom}
+        aspect={1}
+        onCropChange={setCrop}
+        onCropComplete={onCropComplete}
+        onZoomChange={setZoom}
+      />
+    )
+  }
+
   return (
     <div className={classNames(className, 'w-full px-3 py-1.5')}>
       <div
@@ -79,15 +110,7 @@ const Uploader: FC<UploaderProps> = ({
               </div>
               <div className="text-xs pointer-events-none">Supports PNG, JPG, JPEG, WEBP and GIF</div>
             </>
-            : <Cropper
-              image={inputImage.url}
-              crop={crop}
-              zoom={zoom}
-              aspect={1}
-              onCropChange={setCrop}
-              onCropComplete={onCropComplete}
-              onZoomChange={setZoom}
-            />
+            : handleShowImage()
         }
       </div>
     </div>

+ 11 - 2
web/app/components/base/app-icon-picker/index.tsx

@@ -74,6 +74,11 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
     setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
   }
 
+  const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
+  const handleUpload = async (file?: File) => {
+    setUploadImageInfo({ file })
+  }
+
   const handleSelect = async () => {
     if (activeTab === 'emoji') {
       if (emoji) {
@@ -85,9 +90,13 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
       }
     }
     else {
-      if (!imageCropInfo)
+      if (!imageCropInfo && !uploadImageInfo)
         return
       setUploading(true)
+      if (imageCropInfo.file) {
+        handleLocalFileUpload(imageCropInfo.file)
+        return
+      }
       const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
       const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
       handleLocalFileUpload(file)
@@ -121,7 +130,7 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
     <Divider className='m-0' />
 
     <EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} />
-    <Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} />
+    <Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
 
     <Divider className='m-0' />
     <div className='w-full flex items-center justify-center p-3 gap-2'>

+ 49 - 0
web/app/components/base/app-icon-picker/utils.ts

@@ -115,3 +115,52 @@ export default async function getCroppedImg(
     }, mimeType)
   })
 }
+
+export function checkIsAnimatedImage(file) {
+  return new Promise((resolve, reject) => {
+    const fileReader = new FileReader()
+
+    fileReader.onload = function (e) {
+      const arr = new Uint8Array(e.target.result)
+
+      // Check file extension
+      const fileName = file.name.toLowerCase()
+      if (fileName.endsWith('.gif')) {
+        // If file is a GIF, assume it's animated
+        resolve(true)
+      }
+      // Check for WebP signature (RIFF and WEBP)
+      else if (isWebP(arr)) {
+        resolve(checkWebPAnimation(arr)) // Check if it's animated
+      }
+      else {
+        resolve(false) // Not a GIF or WebP
+      }
+    }
+
+    fileReader.onerror = function (err) {
+      reject(err) // Reject the promise on error
+    }
+
+    // Read the file as an array buffer
+    fileReader.readAsArrayBuffer(file)
+  })
+}
+
+// Function to check for WebP signature
+function isWebP(arr) {
+  return (
+    arr[0] === 0x52 && arr[1] === 0x49 && arr[2] === 0x46 && arr[3] === 0x46
+    && arr[8] === 0x57 && arr[9] === 0x45 && arr[10] === 0x42 && arr[11] === 0x50
+  ) // "WEBP"
+}
+
+// Function to check if the WebP is animated (contains ANIM chunk)
+function checkWebPAnimation(arr) {
+  // Search for the ANIM chunk in WebP to determine if it's animated
+  for (let i = 12; i < arr.length - 4; i++) {
+    if (arr[i] === 0x41 && arr[i + 1] === 0x4E && arr[i + 2] === 0x49 && arr[i + 3] === 0x4D)
+      return true // Found animation
+  }
+  return false // No animation chunk found
+}