From 594bf969220a7be26e5e6f97f43fe883642e1690 Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Fri, 9 Aug 2024 16:48:36 +0800 Subject: [PATCH] file uploader hooks --- .../components/base/file-uploader/hooks.ts | 161 +++++++++++++++++- .../components/base/file-uploader/store.tsx | 5 +- .../components/base/file-uploader/types.ts | 8 + .../components/base/file-uploader/utils.ts | 2 +- 4 files changed, 171 insertions(+), 5 deletions(-) diff --git a/web/app/components/base/file-uploader/hooks.ts b/web/app/components/base/file-uploader/hooks.ts index 5f96a0974a..2c02b53640 100644 --- a/web/app/components/base/file-uploader/hooks.ts +++ b/web/app/components/base/file-uploader/hooks.ts @@ -1,5 +1,162 @@ +import type { ClipboardEvent } from 'react' +import { + useCallback, + useState, +} from 'react' +import produce from 'immer' +import { v4 as uuid4 } from 'uuid' +import { useTranslation } from 'react-i18next' +import type { TFile } from './types' import { useFileStore } from './store' +import { fileUpload } from './utils' +import { useToastContext } from '@/app/components/base/toast' -export const useFile = () => { - const fileStore = useFileStore() +type UseFileParams = { + isPublicAPI?: boolean + url?: string +} +export const useFile = ({ + isPublicAPI, + url, +}: UseFileParams) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const fileStore = useFileStore() + + const handleAddOrUpdateFiles = useCallback((newFile: TFile) => { + 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 + else + draft.push(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 handleLoadFileFromLink = useCallback((fileId: string, progress: number) => { + const { + files, + setFiles, + } = fileStore.getState() + const newFiles = produce(files, (draft) => { + const index = draft.findIndex(file => file._id === fileId) + + if (index > -1) + draft[index]._progress = progress + }) + setFiles(newFiles) + }, [fileStore]) + + const handleClearFiles = useCallback(() => { + const { + setFiles, + } = fileStore.getState() + setFiles([]) + }, [fileStore]) + + const handleLocalFileUpload = useCallback((file: File) => { + const reader = new FileReader() + reader.addEventListener( + 'load', + () => { + const imageFile = { + _id: uuid4(), + file, + _url: reader.result as string, + _progress: 0, + } + handleAddOrUpdateFiles(imageFile) + fileUpload({ + file: imageFile.file, + onProgressCallback: (progress) => { + handleAddOrUpdateFiles({ ...imageFile, _progress: progress }) + }, + onSuccessCallback: (res) => { + handleAddOrUpdateFiles({ ...imageFile, _fileId: res.id, _progress: 100 }) + }, + onErrorCallback: () => { + notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerUploadError') }) + handleAddOrUpdateFiles({ ...imageFile, _progress: -1 }) + }, + }, isPublicAPI, url) + }, + false, + ) + reader.addEventListener( + 'error', + () => { + notify({ type: 'error', message: t('common.imageUploader.uploadFromComputerReadError') }) + }, + false, + ) + reader.readAsDataURL(file) + }, [notify, t, handleAddOrUpdateFiles, isPublicAPI, url]) + + const handleClipboardPasteFile = useCallback((e: ClipboardEvent) => { + const file = e.clipboardData?.files[0] + if (file) { + e.preventDefault() + handleLocalFileUpload(file) + } + }, [handleLocalFileUpload]) + + const [isDragActive, setIsDragActive] = useState(false) + const handleDragFileEnter = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(true) + }, []) + + const handleDragFileOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + }, []) + + const handleDragFileLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + }, []) + + const handleDropFile = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragActive(false) + + const file = e.dataTransfer.files[0] + + if (file) + handleLocalFileUpload(file) + }, [handleLocalFileUpload]) + + return { + handleAddOrUpdateFiles, + handleRemoveFile, + handleLoadFileFromLink, + handleClearFiles, + handleLocalFileUpload, + handleClipboardPasteFile, + isDragActive, + handleDragFileEnter, + handleDragFileOver, + handleDragFileLeave, + handleDropFile, + } } diff --git a/web/app/components/base/file-uploader/store.tsx b/web/app/components/base/file-uploader/store.tsx index 78ade49fd9..ba0b63bc27 100644 --- a/web/app/components/base/file-uploader/store.tsx +++ b/web/app/components/base/file-uploader/store.tsx @@ -7,10 +7,11 @@ import { useStore as useZustandStore, } from 'zustand' import { createStore } from 'zustand/vanilla' +import type { TFile } from './types' type Shape = { - files: any[] - setFiles: (files: any[]) => void + files: TFile[] + setFiles: (files: TFile[]) => void } export const createFileStore = () => { diff --git a/web/app/components/base/file-uploader/types.ts b/web/app/components/base/file-uploader/types.ts index 8a17505eaa..3298d7e7a4 100644 --- a/web/app/components/base/file-uploader/types.ts +++ b/web/app/components/base/file-uploader/types.ts @@ -12,3 +12,11 @@ export enum FileTypeEnum { GIF = 'GIF', OTHER = 'OTHER', } + +export type TFile = { + file: File + _id: string + _fileId?: string + _progress?: number + _url?: string +} diff --git a/web/app/components/base/file-uploader/utils.ts b/web/app/components/base/file-uploader/utils.ts index a82b7fd541..41715bdc74 100644 --- a/web/app/components/base/file-uploader/utils.ts +++ b/web/app/components/base/file-uploader/utils.ts @@ -7,7 +7,7 @@ type FileUploadParams = { onErrorCallback: () => void } type FileUpload = (v: FileUploadParams, isPublic?: boolean, url?: string) => void -export const imageUpload: FileUpload = ({ +export const fileUpload: FileUpload = ({ file, onProgressCallback, onSuccessCallback,