| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  | import { useCallback, useMemo, useRef, useState } from 'react' | 
					
						
							|  |  |  | import type { ClipboardEvent } from 'react' | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  | import { useParams } from 'next/navigation' | 
					
						
							|  |  |  | import { useTranslation } from 'react-i18next' | 
					
						
							|  |  |  | import { imageUpload } from './utils' | 
					
						
							|  |  |  | import { useToastContext } from '@/app/components/base/toast' | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  | import { ALLOW_FILE_EXTENSIONS, TransferMethod } from '@/types/app' | 
					
						
							|  |  |  | import type { ImageFile, VisionSettings } from '@/types/app' | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | export const useImageFiles = () => { | 
					
						
							|  |  |  |   const params = useParams() | 
					
						
							|  |  |  |   const { t } = useTranslation() | 
					
						
							|  |  |  |   const { notify } = useToastContext() | 
					
						
							|  |  |  |   const [files, setFiles] = useState<ImageFile[]>([]) | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |   const filesRef = useRef<ImageFile[]>([]) | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   const handleUpload = (imageFile: ImageFile) => { | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |     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 | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |   } | 
					
						
							|  |  |  |   const handleRemove = (imageFileId: string) => { | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |     const files = filesRef.current | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |     const index = files.findIndex(file => file._id === imageFileId) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (index > -1) { | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |       const currentFile = files[index] | 
					
						
							|  |  |  |       const newFiles = [...files.slice(0, index), { ...currentFile, deleted: true }, ...files.slice(index + 1)] | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |       setFiles(newFiles) | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |       filesRef.current = newFiles | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const handleImageLinkLoadError = (imageFileId: string) => { | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |     const files = filesRef.current | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |     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)] | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |       filesRef.current = newFiles | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |       setFiles(newFiles) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const handleImageLinkLoadSuccess = (imageFileId: string) => { | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |     const files = filesRef.current | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |     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)] | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |       filesRef.current = newFiles | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |       setFiles(newFiles) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   const handleReUpload = (imageFileId: string) => { | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |     const files = filesRef.current | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |     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)] | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |           filesRef.current = newFiles | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |           setFiles(newFiles) | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         onSuccessCallback: (res) => { | 
					
						
							|  |  |  |           const newFiles = [...files.slice(0, index), { ...currentImageFile, fileId: res.id, progress: 100 }, ...files.slice(index + 1)] | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |           filesRef.current = newFiles | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |           setFiles(newFiles) | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         onErrorCallback: () => { | 
					
						
							|  |  |  |           notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') }) | 
					
						
							|  |  |  |           const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)] | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |           filesRef.current = newFiles | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |           setFiles(newFiles) | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |       }, !!params.token) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const handleClear = () => { | 
					
						
							|  |  |  |     setFiles([]) | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |     filesRef.current = [] | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |   const filteredFiles = useMemo(() => { | 
					
						
							|  |  |  |     return files.filter(file => !file.deleted) | 
					
						
							|  |  |  |   }, [files]) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |   return { | 
					
						
							| 
									
										
										
										
											2023-11-16 11:56:11 +08:00
										 |  |  |     files: filteredFiles, | 
					
						
							| 
									
										
										
										
											2023-11-13 22:32:39 +08:00
										 |  |  |     onUpload: handleUpload, | 
					
						
							|  |  |  |     onRemove: handleRemove, | 
					
						
							|  |  |  |     onImageLinkLoadError: handleImageLinkLoadError, | 
					
						
							|  |  |  |     onImageLinkLoadSuccess: handleImageLinkLoadSuccess, | 
					
						
							|  |  |  |     onReUpload: handleReUpload, | 
					
						
							|  |  |  |     onClear: handleClear, | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  | type useLocalUploaderProps = { | 
					
						
							|  |  |  |   disabled?: boolean | 
					
						
							|  |  |  |   limit?: number | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  |   onUpload: (imageFile: ImageFile) => void | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  | export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useLocalUploaderProps) => { | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  |   const { notify } = useToastContext() | 
					
						
							|  |  |  |   const params = useParams() | 
					
						
							|  |  |  |   const { t } = useTranslation() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  |   const handleLocalFileUpload = useCallback((file: File) => { | 
					
						
							|  |  |  |     if (disabled) { | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  |       // TODO: leave some warnings?
 | 
					
						
							|  |  |  |       return | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  |     if (!ALLOW_FILE_EXTENSIONS.includes(file.type.split('/')[1])) | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  |       return | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  |     if (limit && file.size > limit * 1024 * 1024) { | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  |       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) | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  |   }, [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>) => { | 
					
						
							| 
									
										
										
										
											2023-12-05 16:34:12 +08:00
										 |  |  |     // reserve native text copy behavior
 | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  |     const file = e.clipboardData?.files[0] | 
					
						
							| 
									
										
										
										
											2023-12-05 16:34:12 +08:00
										 |  |  |     // when copyed file, prevent default action
 | 
					
						
							|  |  |  |     if (file) { | 
					
						
							|  |  |  |       e.preventDefault() | 
					
						
							|  |  |  |       handleLocalFileUpload(file) | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  |   }, [handleLocalFileUpload]) | 
					
						
							| 
									
										
										
										
											2023-12-01 10:04:14 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return { | 
					
						
							|  |  |  |     onPaste: handleClipboardPaste, | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2023-12-01 16:50:22 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 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, | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } |