mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-09-05 23:57:19 +08:00
feat: Add file drag-and-drop functionality #25
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useRef, useState } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { theme, Pagination } from 'antd';
|
import { theme, Pagination } from 'antd';
|
||||||
import { AppWindowsLayer } from '../../apps/AppWindowsLayer';
|
import { AppWindowsLayer } from '../../apps/AppWindowsLayer';
|
||||||
@@ -15,6 +15,7 @@ import { GridView } from './components/GridView';
|
|||||||
import { FileListView } from './components/FileListView';
|
import { FileListView } from './components/FileListView';
|
||||||
import { EmptyState } from './components/EmptyState';
|
import { EmptyState } from './components/EmptyState';
|
||||||
import { ContextMenu } from './components/ContextMenu';
|
import { ContextMenu } from './components/ContextMenu';
|
||||||
|
import { DropzoneOverlay } from './components/DropzoneOverlay';
|
||||||
import { CreateDirModal } from './components/Modals/CreateDirModal';
|
import { CreateDirModal } from './components/Modals/CreateDirModal';
|
||||||
import { RenameModal } from './components/Modals/RenameModal';
|
import { RenameModal } from './components/Modals/RenameModal';
|
||||||
import { ProcessorModal } from './components/Modals/ProcessorModal';
|
import { ProcessorModal } from './components/Modals/ProcessorModal';
|
||||||
@@ -29,6 +30,8 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
const { navKey = 'files', '*': restPath = '' } = useParams();
|
const { navKey = 'files', '*': restPath = '' } = useParams();
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const dragCounter = useRef(0);
|
||||||
|
|
||||||
// --- Hooks ---
|
// --- Hooks ---
|
||||||
const { path, entries, loading, pagination, processorTypes, load, navigateTo, goUp, handlePaginationChange, refresh } = useFileExplorer(navKey);
|
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 { appWindows, openFileWithDefaultApp, confirmOpenWithApp, closeWindow, toggleMax, bringToFront, updateWindow } = useAppWindows(path);
|
||||||
const { ctxMenu, blankCtxMenu, openContextMenu, openBlankContextMenu, closeContextMenus } = useContextMenu();
|
const { ctxMenu, blankCtxMenu, openContextMenu, openBlankContextMenu, closeContextMenus } = useContextMenu();
|
||||||
const uploader = useUploader(path, refresh);
|
const uploader = useUploader(path, refresh);
|
||||||
|
const { handleFileDrop } = uploader;
|
||||||
const processorHook = useProcessor({ path, processorTypes, refresh });
|
const processorHook = useProcessor({ path, processorTypes, refresh });
|
||||||
const { thumbs } = useThumbnails(entries, path);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -91,6 +126,10 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
position: 'relative'
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
onClick={closeContextMenus}
|
onClick={closeContextMenus}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<Header
|
<Header
|
||||||
navKey={navKey}
|
navKey={navKey}
|
||||||
@@ -212,6 +251,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
onStartUpload={uploader.startUpload}
|
onStartUpload={uploader.startUpload}
|
||||||
/>
|
/>
|
||||||
<AppWindowsLayer windows={appWindows} onClose={closeWindow} onToggleMax={toggleMax} onBringToFront={bringToFront} onUpdateWindow={updateWindow} />
|
<AppWindowsLayer windows={appWindows} onClose={closeWindow} onToggleMax={toggleMax} onBringToFront={bringToFront} onUpdateWindow={updateWindow} />
|
||||||
|
<DropzoneOverlay visible={isDragging} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -39,13 +39,25 @@ export function useUploader(path: string, onUploadComplete: () => void) {
|
|||||||
}));
|
}));
|
||||||
setFiles(newFiles);
|
setFiles(newFiles);
|
||||||
setIsModalVisible(true);
|
setIsModalVisible(true);
|
||||||
// reset file input
|
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = '';
|
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 () => {
|
const startUpload = useCallback(async () => {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -66,7 +78,7 @@ export function useUploader(path: string, onUploadComplete: () => void) {
|
|||||||
setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, progress } : f));
|
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);
|
const permanentLink = vfsApi.getTempPublicUrl(link.token);
|
||||||
|
|
||||||
setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'success', progress: 100, permanentLink } : f));
|
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,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
handleFileChange,
|
handleFileChange,
|
||||||
|
handleFileDrop,
|
||||||
startUpload,
|
startUpload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user