feat: Add file drag-and-drop functionality #25

This commit is contained in:
shiyu
2025-09-01 13:38:51 +08:00
parent 6a52fa3fd5
commit 9b0dd13816
3 changed files with 95 additions and 3 deletions

View File

@@ -1,4 +1,4 @@
import { memo, useEffect, useState } from 'react';
import { memo, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router';
import { theme, Pagination } from 'antd';
import { AppWindowsLayer } from '../../apps/AppWindowsLayer';
@@ -15,6 +15,7 @@ import { GridView } from './components/GridView';
import { FileListView } from './components/FileListView';
import { EmptyState } from './components/EmptyState';
import { ContextMenu } from './components/ContextMenu';
import { DropzoneOverlay } from './components/DropzoneOverlay';
import { CreateDirModal } from './components/Modals/CreateDirModal';
import { RenameModal } from './components/Modals/RenameModal';
import { ProcessorModal } from './components/Modals/ProcessorModal';
@@ -29,6 +30,8 @@ const FileExplorerPage = memo(function FileExplorerPage() {
const { navKey = 'files', '*': restPath = '' } = useParams();
const { token } = theme.useToken();
const [viewMode, setViewMode] = useState<ViewMode>('grid');
const [isDragging, setIsDragging] = useState(false);
const dragCounter = useRef(0);
// --- Hooks ---
const { path, entries, loading, pagination, processorTypes, load, navigateTo, goUp, handlePaginationChange, refresh } = useFileExplorer(navKey);
@@ -37,6 +40,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
const { appWindows, openFileWithDefaultApp, confirmOpenWithApp, closeWindow, toggleMax, bringToFront, updateWindow } = useAppWindows(path);
const { ctxMenu, blankCtxMenu, openContextMenu, openBlankContextMenu, closeContextMenus } = useContextMenu();
const uploader = useUploader(path, refresh);
const { handleFileDrop } = uploader;
const processorHook = useProcessor({ path, processorTypes, refresh });
const { thumbs } = useThumbnails(entries, path);
@@ -79,6 +83,37 @@ const FileExplorerPage = memo(function FileExplorerPage() {
}
};
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragCounter.current++;
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
setIsDragging(true);
}
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragCounter.current--;
if (dragCounter.current === 0) {
setIsDragging(false);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
dragCounter.current = 0;
handleFileDrop(e.dataTransfer.files);
};
return (
<div
style={{
@@ -91,6 +126,10 @@ const FileExplorerPage = memo(function FileExplorerPage() {
position: 'relative'
}}
onClick={closeContextMenus}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<Header
navKey={navKey}
@@ -212,6 +251,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
onStartUpload={uploader.startUpload}
/>
<AppWindowsLayer windows={appWindows} onClose={closeWindow} onToggleMax={toggleMax} onBringToFront={bringToFront} onUpdateWindow={updateWindow} />
<DropzoneOverlay visible={isDragging} />
</div>
);
});

View File

@@ -0,0 +1,39 @@
import { memo } from 'react';
import { theme } from 'antd';
interface DropzoneOverlayProps {
visible: boolean;
}
export const DropzoneOverlay = memo(function DropzoneOverlay({ visible }: DropzoneOverlayProps) {
const { token } = theme.useToken();
if (!visible) {
return null;
}
return (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 100,
borderColor: token.colorPrimary,
borderStyle: 'dashed',
borderWidth: 4,
borderRadius: token.borderRadius,
}}
>
<div style={{ color: 'white', fontSize: 24, fontWeight: 'bold' }}>
</div>
</div>
);
});

View File

@@ -39,13 +39,25 @@ export function useUploader(path: string, onUploadComplete: () => void) {
}));
setFiles(newFiles);
setIsModalVisible(true);
// reset file input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const handleFileDrop = (droppedFiles: FileList) => {
if (droppedFiles && droppedFiles.length > 0) {
const newFiles: UploadFile[] = Array.from(droppedFiles).map(file => ({
id: `${file.name}-${Date.now()}`,
file,
status: 'pending',
progress: 0,
}));
setFiles(newFiles);
setIsModalVisible(true);
}
};
const startUpload = useCallback(async () => {
if (files.length === 0) {
return;
@@ -66,7 +78,7 @@ export function useUploader(path: string, onUploadComplete: () => void) {
setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, progress } : f));
});
const link = await vfsApi.getTempLinkToken(dest, 60 * 60 * 24 * 365 * 10); // 10 years
const link = await vfsApi.getTempLinkToken(dest, 60 * 60 * 24 * 365 * 10);
const permanentLink = vfsApi.getTempPublicUrl(link.token);
setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'success', progress: 100, permanentLink } : f));
@@ -86,6 +98,7 @@ export function useUploader(path: string, onUploadComplete: () => void) {
openModal,
closeModal,
handleFileChange,
handleFileDrop,
startUpload,
};
}