| 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) { |
| |
| 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>) => { |
| |
| const file = e.clipboardData?.files[0] |
| |
| 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, |
| } |
| } |
|
|