diff --git a/web/app/components/base/app-icon-picker/Uploader.tsx b/web/app/components/base/app-icon-picker/Uploader.tsx index 4ddaa40447..ba0ef6b2b2 100644 --- a/web/app/components/base/app-icon-picker/Uploader.tsx +++ b/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 = ({ className, onImageCropped, + onUpload, }) => { const [inputImage, setInputImage] = useState<{ file: File; url: string }>() + const [isAnimatedImage, setIsAnimatedImage] = useState(false) useEffect(() => { return () => { if (inputImage) @@ -34,12 +38,19 @@ const Uploader: FC = ({ if (!inputImage) return onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name) + onUpload?.(undefined) } const handleLocalFileInput = (e: ChangeEvent) => { 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 = ({ const inputRef = createRef() + const handleShowImage = () => { + if (isAnimatedImage) { + return ( + + ) + } + + return ( + + ) + } + return (
= ({
Supports PNG, JPG, JPEG, WEBP and GIF
- : + : handleShowImage() }
diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index ba375abdd9..8a10d28653 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -74,6 +74,11 @@ const AppIconPicker: FC = ({ 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 = ({ } } 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 = ({ - +
diff --git a/web/app/components/base/app-icon-picker/utils.ts b/web/app/components/base/app-icon-picker/utils.ts index 14c9ae3f28..99154d56da 100644 --- a/web/app/components/base/app-icon-picker/utils.ts +++ b/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 +}