Spaces:
Paused
Paused
| import { useCallback, useMemo, useRef, useState } from 'react' | |
| import type { ClipboardEvent } from 'react' | |
| import { useParams } from 'next/navigation' | |
| import { useTranslation } from 'react-i18next' | |
| import { imageUpload } from './utils' | |
| import { useToastContext } from '@/app/components/base/toast' | |
| import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app' | |
| import type { ImageFile, VisionSettings } from '@/types/app' | |
| export const useImageFiles = () => { | |
| const params = useParams() | |
| const { t } = useTranslation() | |
| const { notify } = useToastContext() | |
| const [files, setFiles] = useState<ImageFile[]>([]) | |
| const filesRef = useRef<ImageFile[]>([]) | |
| const handleUpload = (imageFile: ImageFile) => { | |
| const files = filesRef.current | |
| const index = files.findIndex(file => file._id === imageFile._id) | |
| if (index > -1) { | |
| const currentFile = files[index] | |
| const newFiles = [...files.slice(0, index), { ...currentFile, ...imageFile }, ...files.slice(index + 1)] | |
| setFiles(newFiles) | |
| filesRef.current = newFiles | |
| } | |
| else { | |
| const newFiles = [...files, imageFile] | |
| setFiles(newFiles) | |
| filesRef.current = newFiles | |
| } | |
| } | |
| const handleRemove = (imageFileId: string) => { | |
| const files = filesRef.current | |
| const index = files.findIndex(file => file._id === imageFileId) | |
| if (index > -1) { | |
| const currentFile = files[index] | |
| const newFiles = [...files.slice(0, index), { ...currentFile, deleted: true }, ...files.slice(index + 1)] | |
| setFiles(newFiles) | |
| filesRef.current = newFiles | |
| } | |
| } | |
| const handleImageLinkLoadError = (imageFileId: string) => { | |
| const files = filesRef.current | |
| const index = files.findIndex(file => file._id === imageFileId) | |
| if (index > -1) { | |
| const currentFile = files[index] | |
| const newFiles = [...files.slice(0, index), { ...currentFile, progress: -1 }, ...files.slice(index + 1)] | |
| filesRef.current = newFiles | |
| setFiles(newFiles) | |
| } | |
| } | |
| const handleImageLinkLoadSuccess = (imageFileId: string) => { | |
| const files = filesRef.current | |
| const index = files.findIndex(file => file._id === imageFileId) | |
| if (index > -1) { | |
| const currentImageFile = files[index] | |
| const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: 100 }, ...files.slice(index + 1)] | |
| filesRef.current = newFiles | |
| setFiles(newFiles) | |
| } | |
| } | |
| const handleReUpload = (imageFileId: string) => { | |
| const files = filesRef.current | |
| const index = files.findIndex(file => file._id === imageFileId) | |
| if (index > -1) { | |
| const currentImageFile = files[index] | |
| imageUpload({ | |
| file: currentImageFile.file!, | |
| onProgressCallback: (progress) => { | |
| const newFiles = [...files.slice(0, index), { ...currentImageFile, progress }, ...files.slice(index + 1)] | |
| filesRef.current = newFiles | |
| setFiles(newFiles) | |
| }, | |
| onSuccessCallback: (res) => { | |
| const newFiles = [...files.slice(0, index), { ...currentImageFile, fileId: res.id, progress: 100 }, ...files.slice(index + 1)] | |
| filesRef.current = newFiles | |
| setFiles(newFiles) | |
| }, | |
| onErrorCallback: () => { | |
| notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') }) | |
| const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)] | |
| filesRef.current = newFiles | |
| setFiles(newFiles) | |
| }, | |
| }, !!params.token) | |
| } | |
| } | |
| const handleClear = () => { | |
| setFiles([]) | |
| filesRef.current = [] | |
| } | |
| const filteredFiles = useMemo(() => { | |
| return files.filter(file => !file.deleted) | |
| }, [files]) | |
| return { | |
| files: filteredFiles, | |
| onUpload: handleUpload, | |
| onRemove: handleRemove, | |
| onImageLinkLoadError: handleImageLinkLoadError, | |
| onImageLinkLoadSuccess: handleImageLinkLoadSuccess, | |
| onReUpload: handleReUpload, | |
| onClear: handleClear, | |
| } | |
| } | |
| type useLocalUploaderProps = { | |
| disabled?: boolean | |
| limit?: number | |
| onUpload: (imageFile: ImageFile) => void | |
| } | |
| export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useLocalUploaderProps) => { | |
| const { notify } = useToastContext() | |
| const params = useParams() | |
| const { t } = useTranslation() | |
| const handleLocalFileUpload = useCallback((file: File) => { | |
| if (disabled) { | |
| // TODO: leave some warnings? | |
| return | |
| } | |
| if (!ALLOW_FILE_EXTENSIONS.includes(file.type.split('/')[1])) | |
| return | |
| if (limit && file.size > limit * 1024 * 1024) { | |
| notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerLimit', { size: limit }) }) | |
| return | |
| } | |
| const reader = new FileReader() | |
| reader.addEventListener( | |
| 'load', | |
| () => { | |
| const imageFile = { | |
| type: TransferMethod.local_file, | |
| _id: `${Date.now()}`, | |
| fileId: '', | |
| file, | |
| url: reader.result as string, | |
| base64Url: reader.result as string, | |
| progress: 0, | |
| } | |
| onUpload(imageFile) | |
| imageUpload({ | |
| file: imageFile.file, | |
| onProgressCallback: (progress) => { | |
| onUpload({ ...imageFile, progress }) | |
| }, | |
| onSuccessCallback: (res) => { | |
| onUpload({ ...imageFile, fileId: res.id, progress: 100 }) | |
| }, | |
| onErrorCallback: () => { | |
| notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') }) | |
| onUpload({ ...imageFile, progress: -1 }) | |
| }, | |
| }, !!params.token) | |
| }, | |
| false, | |
| ) | |
| reader.addEventListener( | |
| 'error', | |
| () => { | |
| notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerReadError') }) | |
| }, | |
| false, | |
| ) | |
| reader.readAsDataURL(file) | |
| }, [disabled, limit, notify, t, onUpload, params.token]) | |
| return { disabled, handleLocalFileUpload } | |
| } | |
| type useClipboardUploaderProps = { | |
| files: ImageFile[] | |
| visionConfig?: VisionSettings | |
| onUpload: (imageFile: ImageFile) => void | |
| } | |
| export const useClipboardUploader = ({ visionConfig, onUpload, files }: useClipboardUploaderProps) => { | |
| const allowLocalUpload = visionConfig?.transfer_methods?.includes(TransferMethod.local_file) | |
| const disabled = useMemo(() => | |
| !visionConfig | |
| || !visionConfig?.enabled | |
| || !allowLocalUpload | |
| || files.length >= visionConfig.number_limits!, | |
| [allowLocalUpload, files.length, visionConfig]) | |
| const limit = useMemo(() => visionConfig ? +visionConfig.image_file_size_limit! : 0, [visionConfig]) | |
| const { handleLocalFileUpload } = useLocalFileUploader({ limit, onUpload, disabled }) | |
| const handleClipboardPaste = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => { | |
| // reserve native text copy behavior | |
| const file = e.clipboardData?.files[0] | |
| // when copied file, prevent default action | |
| if (file) { | |
| e.preventDefault() | |
| handleLocalFileUpload(file) | |
| } | |
| }, [handleLocalFileUpload]) | |
| return { | |
| onPaste: handleClipboardPaste, | |
| } | |
| } | |
| type useDraggableUploaderProps = { | |
| files: ImageFile[] | |
| visionConfig?: VisionSettings | |
| onUpload: (imageFile: ImageFile) => void | |
| } | |
| export const useDraggableUploader = <T extends HTMLElement>({ visionConfig, onUpload, files }: useDraggableUploaderProps) => { | |
| const allowLocalUpload = visionConfig?.transfer_methods?.includes(TransferMethod.local_file) | |
| const disabled = useMemo(() => | |
| !visionConfig | |
| || !visionConfig?.enabled | |
| || !allowLocalUpload | |
| || files.length >= visionConfig.number_limits!, | |
| [allowLocalUpload, files.length, visionConfig]) | |
| const limit = useMemo(() => visionConfig ? +visionConfig.image_file_size_limit! : 0, [visionConfig]) | |
| const { handleLocalFileUpload } = useLocalFileUploader({ disabled, onUpload, limit }) | |
| const [isDragActive, setIsDragActive] = useState(false) | |
| const handleDragEnter = useCallback((e: React.DragEvent<T>) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| if (!disabled) | |
| setIsDragActive(true) | |
| }, [disabled]) | |
| const handleDragOver = useCallback((e: React.DragEvent<T>) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| }, []) | |
| const handleDragLeave = useCallback((e: React.DragEvent<T>) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| setIsDragActive(false) | |
| }, []) | |
| const handleDrop = useCallback((e: React.DragEvent<T>) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| setIsDragActive(false) | |
| const file = e.dataTransfer.files[0] | |
| if (!file) | |
| return | |
| handleLocalFileUpload(file) | |
| }, [handleLocalFileUpload]) | |
| return { | |
| onDragEnter: handleDragEnter, | |
| onDragOver: handleDragOver, | |
| onDragLeave: handleDragLeave, | |
| onDrop: handleDrop, | |
| isDragActive, | |
| } | |
| } | |