Files
PrivyDrop/frontend/components/ClipboardApp/FileUploadHandler.tsx
T
2025-06-22 21:34:54 +08:00

299 lines
9.5 KiB
TypeScript

import React, {
useState,
useEffect,
ChangeEvent,
useRef,
useCallback,
} from "react";
import { Input } from "@/components/ui/input";
import { Upload } from "lucide-react";
import { FileMeta, CustomFile } from "@/types/webrtc";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
// Add this declaration at the top of the file to extend existing types and avoid IDE errors
declare module "@/components/ui/input" {
interface InputProps {
webkitdirectory?: string | boolean;
directory?: string | boolean;
}
}
import { getDictionary } from "@/lib/dictionary";
import { useLocale } from "@/hooks/useLocale";
import type { Messages } from "@/types/messages";
import { en } from "@/constants/messages/en"; // Import English dictionary as default
const traverseFileTree = async (
item: FileSystemEntry,
path = ""
): Promise<CustomFile[]> => {
return new Promise((resolve) => {
// console.log('path',path)//path in ['','test/','test/sub/']
if (item.isFile) {
(item as FileSystemFileEntry).file((file: File) => {
// console.log('file.name',file.name)//file.name in ['Gmail-773240713232313363.txt','link.txt','cvat-serverless部署踩坑及部署模型测试 (1).docx','images.jpg']
// console.log('fullName',path + file.name,'folderName',path.split('/')[0])
const customFile: CustomFile = Object.assign(file, {
fullName: path + file.name,
folderName: path.split("/")[0],
});
resolve([customFile]);
});
} else if (item.isDirectory) {
const dirReader = (item as FileSystemDirectoryEntry).createReader();
let entries: FileSystemEntry[] = [];
const readEntries = () => {
dirReader.readEntries(async (results) => {
if (results.length) {
entries = entries.concat(Array.from(results));
readEntries();
} else {
const newPath = path + item.name + "/";
const subResults = await Promise.all(
entries.map((entry) => traverseFileTree(entry, newPath))
);
// console.log('subResults',subResults)
const files: CustomFile[] = subResults.flat();
// console.log('files',files)
resolve(files); // Removed conditional judgment, directly return processed files
}
});
};
readEntries();
}
});
};
function formatFileChosen(
template: string,
fileNum: number,
folderNum: number
) {
return template
.replace("{fileNum}", fileNum.toString())
.replace("{folderNum}", folderNum.toString());
}
interface FileUploadHandlerProps {
onFilePicked: (files: CustomFile[]) => void;
}
const FileUploadHandler: React.FC<FileUploadHandlerProps> = ({
onFilePicked,
}) => {
const locale = useLocale();
const [messages, setMessages] = useState<Messages>(en); // Use English dictionary as initial value
const dropZoneRef = useRef<HTMLDivElement>(null); // Drag and drop files to attachments -- support
const folderInputRef = useRef<HTMLInputElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// File selector -- message prompt
const [fileText, setFileText] = useState<string>(
en.text.fileUploadHandler.NoFileChosen_tips
);
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
if (locale !== "en") {
// Only load other language packs if not English
getDictionary(locale)
.then((dict) => {
setMessages(dict);
setFileText(dict.text.fileUploadHandler.NoFileChosen_tips);
})
.catch((error) => console.error("Failed to load messages:", error));
}
}, [locale]);
const handleFileChange = useCallback(
(newFiles: CustomFile[]) => {
// console.log(newFiles);
onFilePicked(newFiles);
const fileNum = newFiles.length;
const folderNum = newFiles.filter((file) => file.folderName).length;
const choose_dis = formatFileChosen(
messages!.text.fileUploadHandler.fileChosen_tips_template,
fileNum,
folderNum
);
setFileText(choose_dis);
setTimeout(
() => setFileText(messages!.text.fileUploadHandler.NoFileChosen_tips),
2000
);
// Reset the file input
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
},
[messages, onFilePicked]
);
// Drag and drop folder upload response processing
const handleDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
const items = e.dataTransfer.items;
if (items) {
const itemsArray = Array.from(items);
Promise.all(
itemsArray.map((item) => {
const entry = item.webkitGetAsEntry();
if (entry) {
return traverseFileTree(entry);
}
return Promise.resolve([]);
})
).then((results) => {
const allFiles = results.flat();
handleFileChange(allFiles);
});
}
},
[handleFileChange]
);
/* Define a callback function handleDragOver to handle the drag-over event.
In handleDragOver, prevent default behavior and event propagation to ensure custom handling.
There is no dependency array, which means the handleDragOver function will only be created once when the component first renders, and will not be re-created in subsequent renders.
*/
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
}, []);
// Click to upload file processing
const handleFileInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files = Array.from(e.target.files);
let files2 = [];
for (let file of files) {
const customFile: CustomFile = Object.assign(file, {
fullName: file.name,
folderName: "",
});
files2.push(customFile);
}
handleFileChange(files2);
setIsModalOpen(false); // Close the dialog
}
},
[handleFileChange]
);
// Click to upload folder response processing
const handleFolderInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const files_ = Array.from(e.target.files);
let files: CustomFile[] = [];
files_.forEach((file) => {
// console.log('file.webkitRelativePath',file.webkitRelativePath)//[test/Gmail-773240713232313363.txt,test/link.txt,test/sub/cvat-serverless部署踩坑及部署模型测试 (1).docx,test/sub/images.jpg]
const pathParts = file.webkitRelativePath.split("/");
const customFile: CustomFile = Object.assign(file, {
fullName: file.webkitRelativePath,
folderName: pathParts[0],
});
files.push(customFile);
});
handleFileChange(files);
setIsModalOpen(false); // Close the dialog
}
},
[handleFileChange]
);
// Handle drag and drop area click
const handleZoneClick = () => {
setIsModalOpen(true);
};
// Handle file selection
const handleSelectFile = () => {
fileInputRef.current?.click();
};
// Handle folder selection
const handleSelectFolder = () => {
folderInputRef.current?.click();
};
if (messages === null) {
return <div>Loading...</div>;
}
return (
<>
<div
ref={dropZoneRef}
onDrop={handleDrop}
onDragOver={handleDragOver}
className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer"
onClick={handleZoneClick}
>
<p className="text-sm text-gray-600 mb-4">
{messages.text.fileUploadHandler.Drag_tips}
</p>
<Upload className="h-12 w-12 mx-auto mb-4 text-blue-500" />
<p className="text-sm text-gray-600">{fileText}</p>
<Input
id="file-upload"
type="file"
onChange={handleFileInputChange}
multiple
className="hidden"
ref={fileInputRef}
/>
<Input
id="folder-upload"
type="file"
onChange={handleFolderInputChange}
multiple
webkitdirectory=""
directory=""
className="hidden"
ref={folderInputRef}
/>
</div>
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
{messages.text.fileUploadHandler.chosenDiagTitle}
</DialogTitle>
<DialogDescription className="mt-2 text-muted-foreground">
{messages.text.fileUploadHandler.chosenDiagDescription}
</DialogDescription>
</DialogHeader>
<div className="flex justify-center gap-4 mt-6">
<button
onClick={handleSelectFile}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors"
>
{messages.text.fileUploadHandler.SelectFile_dis}
</button>
<button
onClick={handleSelectFolder}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors"
>
{messages.text.fileUploadHandler.SelectFolder_dis}
</button>
</div>
</DialogContent>
</Dialog>
</>
);
};
export { FileUploadHandler };