Implement modern file manager with drag-and-drop interface
Major UI/UX improvements to replace clunky sidebar with modern grid layout: - Add FileManagerModern component with grid-based file browser - Implement drag-and-drop file upload with validation and progress - Add comprehensive context menu with file operations (copy/cut/paste/delete) - Create intelligent file selection system with multi-select support - Add modern toolbar with search, view switching, and file operations - Integrate seamless view switching between classic and modern interfaces - Support keyboard shortcuts and accessibility features - Add complete i18n support for all new interface elements Technical components: - FileManagerGrid: Grid layout with breadcrumb navigation - FileManagerContextMenu: Right-click context menu system - useFileSelection: Hook for managing file selection state - useDragAndDrop: Hook for handling drag-and-drop operations - View switching logic integrated into main FileManager component The modern interface is now the default while maintaining backward compatibility. Users can switch between modern and classic views seamlessly.
This commit is contained in:
328
src/ui/Desktop/Apps/File Manager/FileManagerContextMenu.tsx
Normal file
328
src/ui/Desktop/Apps/File Manager/FileManagerContextMenu.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Download,
|
||||
Edit3,
|
||||
Copy,
|
||||
Scissors,
|
||||
Trash2,
|
||||
Info,
|
||||
Upload,
|
||||
FolderPlus,
|
||||
FilePlus,
|
||||
RefreshCw,
|
||||
Clipboard,
|
||||
Eye,
|
||||
Share
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface FileItem {
|
||||
name: string;
|
||||
type: "file" | "directory" | "link";
|
||||
path: string;
|
||||
size?: number;
|
||||
modified?: string;
|
||||
permissions?: string;
|
||||
owner?: string;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
files: FileItem[];
|
||||
isVisible: boolean;
|
||||
onClose: () => void;
|
||||
onDownload?: (files: FileItem[]) => void;
|
||||
onRename?: (file: FileItem) => void;
|
||||
onCopy?: (files: FileItem[]) => void;
|
||||
onCut?: (files: FileItem[]) => void;
|
||||
onDelete?: (files: FileItem[]) => void;
|
||||
onProperties?: (file: FileItem) => void;
|
||||
onUpload?: () => void;
|
||||
onNewFolder?: () => void;
|
||||
onNewFile?: () => void;
|
||||
onRefresh?: () => void;
|
||||
onPaste?: () => void;
|
||||
onPreview?: (file: FileItem) => void;
|
||||
hasClipboard?: boolean;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
action: () => void;
|
||||
shortcut?: string;
|
||||
separator?: boolean;
|
||||
disabled?: boolean;
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
export function FileManagerContextMenu({
|
||||
x,
|
||||
y,
|
||||
files,
|
||||
isVisible,
|
||||
onClose,
|
||||
onDownload,
|
||||
onRename,
|
||||
onCopy,
|
||||
onCut,
|
||||
onDelete,
|
||||
onProperties,
|
||||
onUpload,
|
||||
onNewFolder,
|
||||
onNewFile,
|
||||
onRefresh,
|
||||
onPaste,
|
||||
onPreview,
|
||||
hasClipboard = false
|
||||
}: ContextMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const [menuPosition, setMenuPosition] = useState({ x, y });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
|
||||
// 调整菜单位置避免超出屏幕
|
||||
const adjustPosition = () => {
|
||||
const menuWidth = 200;
|
||||
const menuHeight = 300;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let adjustedX = x;
|
||||
let adjustedY = y;
|
||||
|
||||
if (x + menuWidth > viewportWidth) {
|
||||
adjustedX = viewportWidth - menuWidth - 10;
|
||||
}
|
||||
|
||||
if (y + menuHeight > viewportHeight) {
|
||||
adjustedY = viewportHeight - menuHeight - 10;
|
||||
}
|
||||
|
||||
setMenuPosition({ x: adjustedX, y: adjustedY });
|
||||
};
|
||||
|
||||
adjustPosition();
|
||||
|
||||
// 点击外部关闭菜单
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 键盘支持
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isVisible, x, y, onClose]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
const isFileContext = files.length > 0;
|
||||
const isSingleFile = files.length === 1;
|
||||
const isMultipleFiles = files.length > 1;
|
||||
const hasFiles = files.some(f => f.type === 'file');
|
||||
const hasDirectories = files.some(f => f.type === 'directory');
|
||||
|
||||
// 构建菜单项
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if (isFileContext) {
|
||||
// 文件/文件夹选中时的菜单
|
||||
if (hasFiles && onPreview) {
|
||||
menuItems.push({
|
||||
icon: <Eye className="w-4 h-4" />,
|
||||
label: t("fileManager.preview"),
|
||||
action: () => onPreview(files[0]),
|
||||
disabled: !isSingleFile || files[0].type !== 'file'
|
||||
});
|
||||
}
|
||||
|
||||
if (hasFiles && onDownload) {
|
||||
menuItems.push({
|
||||
icon: <Download className="w-4 h-4" />,
|
||||
label: isMultipleFiles
|
||||
? t("fileManager.downloadFiles", { count: files.length })
|
||||
: t("fileManager.downloadFile"),
|
||||
action: () => onDownload(files),
|
||||
shortcut: "Ctrl+D"
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
|
||||
if (isSingleFile && onRename) {
|
||||
menuItems.push({
|
||||
icon: <Edit3 className="w-4 h-4" />,
|
||||
label: t("fileManager.rename"),
|
||||
action: () => onRename(files[0]),
|
||||
shortcut: "F2"
|
||||
});
|
||||
}
|
||||
|
||||
if (onCopy) {
|
||||
menuItems.push({
|
||||
icon: <Copy className="w-4 h-4" />,
|
||||
label: isMultipleFiles
|
||||
? t("fileManager.copyFiles", { count: files.length })
|
||||
: t("fileManager.copy"),
|
||||
action: () => onCopy(files),
|
||||
shortcut: "Ctrl+C"
|
||||
});
|
||||
}
|
||||
|
||||
if (onCut) {
|
||||
menuItems.push({
|
||||
icon: <Scissors className="w-4 h-4" />,
|
||||
label: isMultipleFiles
|
||||
? t("fileManager.cutFiles", { count: files.length })
|
||||
: t("fileManager.cut"),
|
||||
action: () => onCut(files),
|
||||
shortcut: "Ctrl+X"
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
|
||||
if (onDelete) {
|
||||
menuItems.push({
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
label: isMultipleFiles
|
||||
? t("fileManager.deleteFiles", { count: files.length })
|
||||
: t("fileManager.delete"),
|
||||
action: () => onDelete(files),
|
||||
shortcut: "Delete",
|
||||
danger: true
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
|
||||
if (isSingleFile && onProperties) {
|
||||
menuItems.push({
|
||||
icon: <Info className="w-4 h-4" />,
|
||||
label: t("fileManager.properties"),
|
||||
action: () => onProperties(files[0])
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 空白区域右键菜单
|
||||
if (onUpload) {
|
||||
menuItems.push({
|
||||
icon: <Upload className="w-4 h-4" />,
|
||||
label: t("fileManager.uploadFile"),
|
||||
action: onUpload,
|
||||
shortcut: "Ctrl+U"
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
|
||||
if (onNewFolder) {
|
||||
menuItems.push({
|
||||
icon: <FolderPlus className="w-4 h-4" />,
|
||||
label: t("fileManager.newFolder"),
|
||||
action: onNewFolder,
|
||||
shortcut: "Ctrl+Shift+N"
|
||||
});
|
||||
}
|
||||
|
||||
if (onNewFile) {
|
||||
menuItems.push({
|
||||
icon: <FilePlus className="w-4 h-4" />,
|
||||
label: t("fileManager.newFile"),
|
||||
action: onNewFile,
|
||||
shortcut: "Ctrl+N"
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
|
||||
if (onRefresh) {
|
||||
menuItems.push({
|
||||
icon: <RefreshCw className="w-4 h-4" />,
|
||||
label: t("fileManager.refresh"),
|
||||
action: onRefresh,
|
||||
shortcut: "F5"
|
||||
});
|
||||
}
|
||||
|
||||
if (hasClipboard && onPaste) {
|
||||
menuItems.push({
|
||||
icon: <Clipboard className="w-4 h-4" />,
|
||||
label: t("fileManager.paste"),
|
||||
action: onPaste,
|
||||
shortcut: "Ctrl+V"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className="absolute bg-dark-bg border border-dark-border rounded-lg shadow-lg py-1 min-w-[180px] max-w-[250px] z-50"
|
||||
style={{
|
||||
left: menuPosition.x,
|
||||
top: menuPosition.y
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{menuItems.map((item, index) => {
|
||||
if (item.separator) {
|
||||
return (
|
||||
<div
|
||||
key={`separator-${index}`}
|
||||
className="border-t border-dark-border my-1"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className={cn(
|
||||
"w-full px-3 py-2 text-left text-sm flex items-center justify-between",
|
||||
"hover:bg-dark-hover transition-colors",
|
||||
item.disabled && "opacity-50 cursor-not-allowed",
|
||||
item.danger && "text-red-400 hover:bg-red-500/10"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!item.disabled) {
|
||||
item.action();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
{item.shortcut && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{item.shortcut}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user