Skip to content

Commit

Permalink
fix: cannot upload animated webp image as app icon (#11453)
Browse files Browse the repository at this point in the history
  • Loading branch information
xuzuodong authored Dec 8, 2024
1 parent 7e1184c commit 266d32b
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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>) => {
Expand All @@ -48,7 +50,7 @@ const Uploader: FC<UploaderProps> = ({
checkIsAnimatedImage(file).then((isAnimatedImage) => {
setIsAnimatedImage(!!isAnimatedImage)
if (isAnimatedImage)
onUpload?.(file)
onImageInput?.(false, file)
})
}
}
Expand Down Expand Up @@ -117,4 +119,4 @@ const Uploader: FC<UploaderProps> = ({
)
}

export default Uploader
export default ImageInput
31 changes: 17 additions & 14 deletions web/app/components/base/app-icon-picker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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'>
Expand Down
8 changes: 4 additions & 4 deletions web/app/components/base/app-icon-picker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -148,15 +148,15 @@ 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
) // "WEBP"
}

// 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)
Expand Down

0 comments on commit 266d32b

Please sign in to comment.