Co-authored-by: xuanson9699 <84961581+xuanson9699@users.noreply.github.com>tags/0.11.1
| import { ImagePlus } from '../icons/src/vender/line/images' | import { ImagePlus } from '../icons/src/vender/line/images' | ||||
| import { useDraggableUploader } from './hooks' | import { useDraggableUploader } from './hooks' | ||||
| import { checkIsAnimatedImage } from './utils' | |||||
| import { ALLOW_FILE_EXTENSIONS } from '@/types/app' | import { ALLOW_FILE_EXTENSIONS } from '@/types/app' | ||||
| type UploaderProps = { | type UploaderProps = { | ||||
| className?: string | className?: string | ||||
| onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void | onImageCropped?: (tempUrl: string, croppedAreaPixels: Area, fileName: string) => void | ||||
| onUpload?: (file?: File) => void | |||||
| } | } | ||||
| const Uploader: FC<UploaderProps> = ({ | const Uploader: FC<UploaderProps> = ({ | ||||
| className, | className, | ||||
| onImageCropped, | onImageCropped, | ||||
| onUpload, | |||||
| }) => { | }) => { | ||||
| const [inputImage, setInputImage] = useState<{ file: File; url: string }>() | const [inputImage, setInputImage] = useState<{ file: File; url: string }>() | ||||
| const [isAnimatedImage, setIsAnimatedImage] = useState<boolean>(false) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| return () => { | return () => { | ||||
| if (inputImage) | if (inputImage) | ||||
| if (!inputImage) | if (!inputImage) | ||||
| return | return | ||||
| onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name) | onImageCropped?.(inputImage.url, croppedAreaPixels, inputImage.file.name) | ||||
| onUpload?.(undefined) | |||||
| } | } | ||||
| const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => { | const handleLocalFileInput = (e: ChangeEvent<HTMLInputElement>) => { | ||||
| const file = e.target.files?.[0] | const file = e.target.files?.[0] | ||||
| if (file) | |||||
| if (file) { | |||||
| setInputImage({ file, url: URL.createObjectURL(file) }) | setInputImage({ file, url: URL.createObjectURL(file) }) | ||||
| checkIsAnimatedImage(file).then((isAnimatedImage) => { | |||||
| setIsAnimatedImage(!!isAnimatedImage) | |||||
| if (isAnimatedImage) | |||||
| onUpload?.(file) | |||||
| }) | |||||
| } | |||||
| } | } | ||||
| const { | const { | ||||
| const inputRef = createRef<HTMLInputElement>() | 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 ( | return ( | ||||
| <div className={classNames(className, 'w-full px-3 py-1.5')}> | <div className={classNames(className, 'w-full px-3 py-1.5')}> | ||||
| <div | <div | ||||
| </div> | </div> | ||||
| <div className="text-xs pointer-events-none">Supports PNG, JPG, JPEG, WEBP and GIF</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> | ||||
| </div> | </div> |
| setImageCropInfo({ tempUrl, croppedAreaPixels, fileName }) | setImageCropInfo({ tempUrl, croppedAreaPixels, fileName }) | ||||
| } | } | ||||
| const [uploadImageInfo, setUploadImageInfo] = useState<{ file?: File }>() | |||||
| const handleUpload = async (file?: File) => { | |||||
| setUploadImageInfo({ file }) | |||||
| } | |||||
| const handleSelect = async () => { | const handleSelect = async () => { | ||||
| if (activeTab === 'emoji') { | if (activeTab === 'emoji') { | ||||
| if (emoji) { | if (emoji) { | ||||
| } | } | ||||
| } | } | ||||
| else { | else { | ||||
| if (!imageCropInfo) | |||||
| if (!imageCropInfo && !uploadImageInfo) | |||||
| return | return | ||||
| setUploading(true) | setUploading(true) | ||||
| if (imageCropInfo.file) { | |||||
| handleLocalFileUpload(imageCropInfo.file) | |||||
| return | |||||
| } | |||||
| const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName) | const blob = await getCroppedImg(imageCropInfo.tempUrl, imageCropInfo.croppedAreaPixels, imageCropInfo.fileName) | ||||
| const file = new File([blob], imageCropInfo.fileName, { type: blob.type }) | const file = new File([blob], imageCropInfo.fileName, { type: blob.type }) | ||||
| handleLocalFileUpload(file) | handleLocalFileUpload(file) | ||||
| <Divider className='m-0' /> | <Divider className='m-0' /> | ||||
| <EmojiPickerInner className={activeTab === 'emoji' ? 'block' : 'hidden'} onSelect={handleSelectEmoji} /> | <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' /> | <Divider className='m-0' /> | ||||
| <div className='w-full flex items-center justify-center p-3 gap-2'> | <div className='w-full flex items-center justify-center p-3 gap-2'> |
| }, mimeType) | }, 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 | |||||
| } |