Co-authored-by: xuanson9699 <84961581+xuanson9699@users.noreply.github.com>tags/0.11.1
| @@ -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> | |||
| @@ -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'> | |||
| @@ -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 | |||
| } | |||