| import type { ClipboardEvent } from 'react' | |
| import { | |
| useCallback, | |
| useState, | |
| } from 'react' | |
| import { useParams } from 'next/navigation' | |
| import produce from 'immer' | |
| import { v4 as uuid4 } from 'uuid' | |
| import { useTranslation } from 'react-i18next' | |
| import type { FileEntity } from './types' | |
| import { useFileStore } from './store' | |
| import { | |
| fileUpload, | |
| getSupportFileType, | |
| isAllowedFileExtension, | |
| } from './utils' | |
| import { | |
| AUDIO_SIZE_LIMIT, | |
| FILE_SIZE_LIMIT, | |
| IMG_SIZE_LIMIT, | |
| MAX_FILE_UPLOAD_LIMIT, | |
| VIDEO_SIZE_LIMIT, | |
| } from '@/app/components/base/file-uploader/constants' | |
| import { useToastContext } from '@/app/components/base/toast' | |
| import { TransferMethod } from '@/types/app' | |
| import { SupportUploadFileTypes } from '@/app/components/workflow/types' | |
| import type { FileUpload } from '@/app/components/base/features/types' | |
| import { formatFileSize } from '@/utils/format' | |
| import { uploadRemoteFileInfo } from '@/service/common' | |
| import type { FileUploadConfigResponse } from '@/models/common' | |
| export const useFileSizeLimit = (fileUploadConfig?: FileUploadConfigResponse) => { | |
| const imgSizeLimit = Number(fileUploadConfig?.image_file_size_limit) * 1024 * 1024 || IMG_SIZE_LIMIT | |
| const docSizeLimit = Number(fileUploadConfig?.file_size_limit) * 1024 * 1024 || FILE_SIZE_LIMIT | |
| const audioSizeLimit = Number(fileUploadConfig?.audio_file_size_limit) * 1024 * 1024 || AUDIO_SIZE_LIMIT | |
| const videoSizeLimit = Number(fileUploadConfig?.video_file_size_limit) * 1024 * 1024 || VIDEO_SIZE_LIMIT | |
| const maxFileUploadLimit = Number(fileUploadConfig?.workflow_file_upload_limit) || MAX_FILE_UPLOAD_LIMIT | |
| return { | |
| imgSizeLimit, | |
| docSizeLimit, | |
| audioSizeLimit, | |
| videoSizeLimit, | |
| maxFileUploadLimit, | |
| } | |
| } | |
| export const useFile = (fileConfig: FileUpload) => { | |
| const { t } = useTranslation() | |
| const { notify } = useToastContext() | |
| const fileStore = useFileStore() | |
| const params = useParams() | |
| const { imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit } = useFileSizeLimit(fileConfig.fileUploadConfig) | |
| const checkSizeLimit = useCallback((fileType: string, fileSize: number) => { | |
| switch (fileType) { | |
| case SupportUploadFileTypes.image: { | |
| if (fileSize > imgSizeLimit) { | |
| notify({ | |
| type: 'error', | |
| message: t('common.fileUploader.uploadFromComputerLimit', { | |
| type: SupportUploadFileTypes.image, | |
| size: formatFileSize(imgSizeLimit), | |
| }), | |
| }) | |
| return false | |
| } | |
| return true | |
| } | |
| case SupportUploadFileTypes.document: { | |
| if (fileSize > docSizeLimit) { | |
| notify({ | |
| type: 'error', | |
| message: t('common.fileUploader.uploadFromComputerLimit', { | |
| type: SupportUploadFileTypes.document, | |
| size: formatFileSize(docSizeLimit), | |
| }), | |
| }) | |
| return false | |
| } | |
| return true | |
| } | |
| case SupportUploadFileTypes.audio: { | |
| if (fileSize > audioSizeLimit) { | |
| notify({ | |
| type: 'error', | |
| message: t('common.fileUploader.uploadFromComputerLimit', { | |
| type: SupportUploadFileTypes.audio, | |
| size: formatFileSize(audioSizeLimit), | |
| }), | |
| }) | |
| return false | |
| } | |
| return true | |
| } | |
| case SupportUploadFileTypes.video: { | |
| if (fileSize > videoSizeLimit) { | |
| notify({ | |
| type: 'error', | |
| message: t('common.fileUploader.uploadFromComputerLimit', { | |
| type: SupportUploadFileTypes.video, | |
| size: formatFileSize(videoSizeLimit), | |
| }), | |
| }) | |
| return false | |
| } | |
| return true | |
| } | |
| case SupportUploadFileTypes.custom: { | |
| if (fileSize > docSizeLimit) { | |
| notify({ | |
| type: 'error', | |
| message: t('common.fileUploader.uploadFromComputerLimit', { | |
| type: SupportUploadFileTypes.document, | |
| size: formatFileSize(docSizeLimit), | |
| }), | |
| }) | |
| return false | |
| } | |
| return true | |
| } | |
| default: { | |
| return true | |
| } | |
| } | |
| }, [audioSizeLimit, docSizeLimit, imgSizeLimit, notify, t, videoSizeLimit]) | |
| const handleAddFile = useCallback((newFile: FileEntity) => { | |
| const { | |
| files, | |
| setFiles, | |
| } = fileStore.getState() | |
| const newFiles = produce(files, (draft) => { | |
| draft.push(newFile) | |
| }) | |
| setFiles(newFiles) | |
| }, [fileStore]) | |
| const handleUpdateFile = useCallback((newFile: FileEntity) => { | |
| const { | |
| files, | |
| setFiles, | |
| } = fileStore.getState() | |
| const newFiles = produce(files, (draft) => { | |
| const index = draft.findIndex(file => file.id === newFile.id) | |
| if (index > -1) | |
| draft[index] = newFile | |
| }) | |
| setFiles(newFiles) | |
| }, [fileStore]) | |
| const handleRemoveFile = useCallback((fileId: string) => { | |
| const { | |
| files, | |
| setFiles, | |
| } = fileStore.getState() | |
| const newFiles = files.filter(file => file.id !== fileId) | |
| setFiles(newFiles) | |
| }, [fileStore]) | |
| const handleReUploadFile = useCallback((fileId: string) => { | |
| const { | |
| files, | |
| setFiles, | |
| } = fileStore.getState() | |
| const index = files.findIndex(file => file.id === fileId) | |
| if (index > -1) { | |
| const uploadingFile = files[index] | |
| const newFiles = produce(files, (draft) => { | |
| draft[index].progress = 0 | |
| }) | |
| setFiles(newFiles) | |
| fileUpload({ | |
| file: uploadingFile.originalFile!, | |
| onProgressCallback: (progress) => { | |
| handleUpdateFile({ ...uploadingFile, progress }) | |
| }, | |
| onSuccessCallback: (res) => { | |
| handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 }) | |
| }, | |
| onErrorCallback: () => { | |
| notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') }) | |
| handleUpdateFile({ ...uploadingFile, progress: -1 }) | |
| }, | |
| }, !!params.token) | |
| } | |
| }, [fileStore, notify, t, handleUpdateFile, params]) | |
| const startProgressTimer = useCallback((fileId: string) => { | |
| const timer = setInterval(() => { | |
| const files = fileStore.getState().files | |
| const file = files.find(file => file.id === fileId) | |
| if (file && file.progress < 80 && file.progress >= 0) | |
| handleUpdateFile({ ...file, progress: file.progress + 20 }) | |
| else | |
| clearTimeout(timer) | |
| }, 200) | |
| }, [fileStore, handleUpdateFile]) | |
| const handleLoadFileFromLink = useCallback((url: string) => { | |
| const allowedFileTypes = fileConfig.allowed_file_types | |
| const uploadingFile = { | |
| id: uuid4(), | |
| name: url, | |
| type: '', | |
| size: 0, | |
| progress: 0, | |
| transferMethod: TransferMethod.local_file, | |
| supportFileType: '', | |
| url, | |
| isRemote: true, | |
| } | |
| handleAddFile(uploadingFile) | |
| startProgressTimer(uploadingFile.id) | |
| uploadRemoteFileInfo(url, !!params.token).then((res) => { | |
| const newFile = { | |
| ...uploadingFile, | |
| type: res.mime_type, | |
| size: res.size, | |
| progress: 100, | |
| supportFileType: getSupportFileType(res.name, res.mime_type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)), | |
| uploadedId: res.id, | |
| url: res.url, | |
| } | |
| if (!isAllowedFileExtension(res.name, res.mime_type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) { | |
| notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') }) | |
| handleRemoveFile(uploadingFile.id) | |
| } | |
| if (!checkSizeLimit(newFile.supportFileType, newFile.size)) | |
| handleRemoveFile(uploadingFile.id) | |
| else | |
| handleUpdateFile(newFile) | |
| }).catch(() => { | |
| notify({ type: 'error', message: t('common.fileUploader.pasteFileLinkInvalid') }) | |
| handleRemoveFile(uploadingFile.id) | |
| }) | |
| }, [checkSizeLimit, handleAddFile, handleUpdateFile, notify, t, handleRemoveFile, fileConfig?.allowed_file_types, fileConfig.allowed_file_extensions, startProgressTimer]) | |
| const handleLoadFileFromLinkSuccess = useCallback(() => { }, []) | |
| const handleLoadFileFromLinkError = useCallback(() => { }, []) | |
| const handleClearFiles = useCallback(() => { | |
| const { | |
| setFiles, | |
| } = fileStore.getState() | |
| setFiles([]) | |
| }, [fileStore]) | |
| const handleLocalFileUpload = useCallback((file: File) => { | |
| if (!isAllowedFileExtension(file.name, file.type, fileConfig.allowed_file_types || [], fileConfig.allowed_file_extensions || [])) { | |
| notify({ type: 'error', message: t('common.fileUploader.fileExtensionNotSupport') }) | |
| return | |
| } | |
| const allowedFileTypes = fileConfig.allowed_file_types | |
| const fileType = getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)) | |
| if (!checkSizeLimit(fileType, file.size)) | |
| return | |
| const reader = new FileReader() | |
| const isImage = file.type.startsWith('image') | |
| reader.addEventListener( | |
| 'load', | |
| () => { | |
| const uploadingFile = { | |
| id: uuid4(), | |
| name: file.name, | |
| type: file.type, | |
| size: file.size, | |
| progress: 0, | |
| transferMethod: TransferMethod.local_file, | |
| supportFileType: getSupportFileType(file.name, file.type, allowedFileTypes?.includes(SupportUploadFileTypes.custom)), | |
| originalFile: file, | |
| base64Url: isImage ? reader.result as string : '', | |
| } | |
| handleAddFile(uploadingFile) | |
| fileUpload({ | |
| file: uploadingFile.originalFile, | |
| onProgressCallback: (progress) => { | |
| handleUpdateFile({ ...uploadingFile, progress }) | |
| }, | |
| onSuccessCallback: (res) => { | |
| handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 }) | |
| }, | |
| onErrorCallback: () => { | |
| notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerUploadError') }) | |
| handleUpdateFile({ ...uploadingFile, progress: -1 }) | |
| }, | |
| }, !!params.token) | |
| }, | |
| false, | |
| ) | |
| reader.addEventListener( | |
| 'error', | |
| () => { | |
| notify({ type: 'error', message: t('common.fileUploader.uploadFromComputerReadError') }) | |
| }, | |
| false, | |
| ) | |
| reader.readAsDataURL(file) | |
| }, [checkSizeLimit, notify, t, handleAddFile, handleUpdateFile, params.token, fileConfig?.allowed_file_types, fileConfig?.allowed_file_extensions]) | |
| const handleClipboardPasteFile = useCallback((e: ClipboardEvent<HTMLTextAreaElement>) => { | |
| const file = e.clipboardData?.files[0] | |
| if (file) { | |
| e.preventDefault() | |
| handleLocalFileUpload(file) | |
| } | |
| }, [handleLocalFileUpload]) | |
| const [isDragActive, setIsDragActive] = useState(false) | |
| const handleDragFileEnter = useCallback((e: React.DragEvent<HTMLElement>) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| setIsDragActive(true) | |
| }, []) | |
| const handleDragFileOver = useCallback((e: React.DragEvent<HTMLElement>) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| }, []) | |
| const handleDragFileLeave = useCallback((e: React.DragEvent<HTMLElement>) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| setIsDragActive(false) | |
| }, []) | |
| const handleDropFile = useCallback((e: React.DragEvent<HTMLElement>) => { | |
| e.preventDefault() | |
| e.stopPropagation() | |
| setIsDragActive(false) | |
| const file = e.dataTransfer.files[0] | |
| if (file) | |
| handleLocalFileUpload(file) | |
| }, [handleLocalFileUpload]) | |
| return { | |
| handleAddFile, | |
| handleUpdateFile, | |
| handleRemoveFile, | |
| handleReUploadFile, | |
| handleLoadFileFromLink, | |
| handleLoadFileFromLinkSuccess, | |
| handleLoadFileFromLinkError, | |
| handleClearFiles, | |
| handleLocalFileUpload, | |
| handleClipboardPasteFile, | |
| isDragActive, | |
| handleDragFileEnter, | |
| handleDragFileOver, | |
| handleDragFileLeave, | |
| handleDropFile, | |
| } | |
| } | |