Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading