Bladeren bron

fix: cannot upload animated webp image as app icon (#11453)

Hash Brown 4 maanden geleden
bovenliggende
commit
266d32bd77

+ 11 - 9
web/app/components/base/app-icon-picker/Uploader.tsx → web/app/components/base/app-icon-picker/ImageInput.tsx

@@ -11,16 +11,19 @@ import { useDraggableUploader } from './hooks'
 import { checkIsAnimatedImage } from './utils'
 import { ALLOW_FILE_EXTENSIONS } from '@/types/app'
 
+export type OnImageInput = {
+  (isCropped: true, tempUrl: string, croppedAreaPixels: Area, fileName: string): void
+  (isCropped: false, file: File): void
+}
+
 type UploaderProps = {
   className?: string
-  onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void
-  onUpload?: (file?: File) => void
+  onImageInput?: OnImageInput
 }
 
-const Uploader: FC<UploaderProps> = ({
+const ImageInput: FC<UploaderProps> = ({
   className,
-  onImageCropped,
-  onUpload,
+  onImageInput,
 }) => {
   const [inputImage, setInputImage] = useState<{ file: File; url: string }>()
   const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false)
@@ -37,8 +40,7 @@ const Uploader: FC<UploaderProps> = ({
   const onCropComplete = async (_: Area, croppedAreaPixels: Area) => {
     if (!inputImage)
       return
-    onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name)
-    onUpload?.(undefined)
+    onImageInput?.(true, inputImage.url, croppedAreaPixels, inputImage.file.name)
   }
 
   const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => {
@@ -48,7 +50,7 @@ const Uploader: FC<UploaderProps> = ({
       checkIsAnimatedImage(file).then((isAnimatedImage) => {
         setIsAnimatedImage(!!isAnimatedImage)
         if (isAnimatedImage)
-          onUpload?.(file)
+          onImageInput?.(false, file)
       })
     }
   }
@@ -117,4 +119,4 @@ const Uploader: FC<UploaderProps> = ({
   )
 }
 
-export default Uploader
+export default ImageInput

+ 17 - 14
web/app/components/base/app-icon-picker/index.tsx

@@ -8,12 +8,14 @@ import Button from '../button'
 import { ImagePlus } from '../icons/src/vender/line/images'
 import { useLocalFileUploader } from '../image-uploader/hooks'
 import EmojiPickerInner from '../emoji-picker/Inner'
-import Uploader from './Uploader'
+import type { OnImageInput } from './ImageInput'
+import ImageInput from './ImageInput'
 import s from './style.module.css'
 import getCroppedImg from './utils'
 import type { AppIconType, ImageFile } from '@/types/app'
 import cn from '@/utils/classnames'
 import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
+
 export type AppIconEmojiSelection = {
   type: 'emoji'
   icon: string
@@ -69,14 +71,15 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
     },
   })
 
-  const [imageCropInfo, setImageCropInfo] = useState<{ tempUrl: string; croppedAreaPixels: Area; fileName: string }>()
-  const handleImageCropped = async (tempUrl: string, croppedAreaPixels: Area, fileName: string) => {
-    setImageCropInfo({ tempUrl, croppedAreaPixels, fileName })
-  }
+  type InputImageInfo = { file: File } | { tempUrl: string; croppedAreaPixels: Area; fileName: string }
+  const [inputImageInfo, setInputImageInfo] = useState<InputImageInfo>()
 
-  const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>()
-  const handleUpload = async (file?: File) => {
-    setUploadImageInfo({ file })
+  const handleImageInput: OnImageInput = async (isCropped: boolean, fileOrTempUrl: string | File, croppedAreaPixels?: Area, fileName?: string) => {
+    setInputImageInfo(
+      isCropped
+        ? { tempUrl: fileOrTempUrl as string, croppedAreaPixels: croppedAreaPixels!, fileName: fileName! }
+        : { file: fileOrTempUrl as File },
+    )
   }
 
   const handleSelect = async () => {
@@ -90,15 +93,15 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
       }
     }
     else {
-      if (!imageCropInfo && !uploadImageInfo)
+      if (!inputImageInfo)
         return
       setUploading(true)
-      if (imageCropInfo.file) {
-        handleLocalFileUpload(imageCropInfo.file)
+      if ('file' in inputImageInfo) {
+        handleLocalFileUpload(inputImageInfo.file)
         return
       }
-      const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName)
-      const file = new File([blob], imageCropInfo.fileName, { type: blob.type })
+      const blob = await getCroppedImg(inputImageInfo.tempUrl, inputImageInfo.croppedAreaPixels, inputImageInfo.fileName)
+      const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
       handleLocalFileUpload(file)
     }
   }
@@ -128,7 +131,7 @@ const AppIconPicker: FC<AppIconPickerProps> = ({
     </div>}
 
     <EmojiPickerInner className={cn(activeTab === 'emoji' ? 'block' : 'hidden', 'pt-2')} onSelect={handleSelectEmoji} />
-    <Uploader className={activeTab === 'image' ? 'block' : 'hidden'} onImageCropped={handleImageCropped} onUpload={handleUpload}/>
+    <ImageInput className={activeTab === 'image' ? 'block' : 'hidden'} onImageInput={handleImageInput} />
 
     <Divider className='m-0' />
     <div className='w-full flex items-center justify-center p-3 gap-2'>

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

@@ -116,12 +116,12 @@ export default async function getCroppedImg(
   })
 }
 
-export function checkIsAnimatedImage(file) {
+export function checkIsAnimatedImage(file: File): Promise<boolean> {
   return new Promise((resolve, reject) => {
     const fileReader = new FileReader()
 
     fileReader.onload = function (e) {
-      const arr = new Uint8Array(e.target.result)
+      const arr = new Uint8Array(e.target?.result as ArrayBuffer)
 
       // Check file extension
       const fileName = file.name.toLowerCase()
@@ -148,7 +148,7 @@ export function checkIsAnimatedImage(file) {
 }
 
 // Function to check for WebP signature
-function isWebP(arr) {
+function isWebP(arr: Uint8Array) {
   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
@@ -156,7 +156,7 @@ function isWebP(arr) {
 }
 
 // Function to check if the WebP is animated (contains ANIM chunk)
-function checkWebPAnimation(arr) {
+function checkWebPAnimation(arr: Uint8Array) {
   // 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)