Translate Chinese comments to English in File Manager components
- Complete translation of FileWindow.tsx comments and hardcoded text - Complete translation of DraggableWindow.tsx hardcoded text - Complete translation of FileManagerSidebar.tsx comments - Complete translation of FileManagerGrid.tsx comments and UI text - Complete translation of DiffViewer.tsx hardcoded text with proper i18n - Partial translation of FileManagerModern.tsx comments (major sections done) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -107,7 +107,7 @@ export function FileManagerContextMenu({
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
|
||||
// 调整菜单位置避免超出屏幕
|
||||
// Adjust menu position to avoid going off screen
|
||||
const adjustPosition = () => {
|
||||
const menuWidth = 200;
|
||||
const menuHeight = 300;
|
||||
@@ -130,13 +130,13 @@ export function FileManagerContextMenu({
|
||||
|
||||
adjustPosition();
|
||||
|
||||
// 延迟添加事件监听器,避免捕获到触发菜单的那次点击
|
||||
// Delay adding event listeners to avoid capturing the click that triggered the menu
|
||||
let cleanupFn: (() => void) | null = null;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
// 点击外部关闭菜单
|
||||
// Click outside to close menu
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// 检查点击是否在菜单内部
|
||||
// Check if click is inside menu
|
||||
const target = event.target as Element;
|
||||
const menuElement = document.querySelector("[data-context-menu]");
|
||||
|
||||
@@ -145,13 +145,13 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
};
|
||||
|
||||
// 右键点击关闭菜单(Windows行为)
|
||||
// Right-click to close menu (Windows behavior)
|
||||
const handleRightClick = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 键盘支持
|
||||
// Keyboard support
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
@@ -159,12 +159,12 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口失焦关闭菜单
|
||||
// Close menu on window blur
|
||||
const handleBlur = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 滚动时关闭菜单(Windows行为)
|
||||
// Close menu on scroll (Windows behavior)
|
||||
const handleScroll = () => {
|
||||
onClose();
|
||||
};
|
||||
@@ -175,7 +175,7 @@ export function FileManagerContextMenu({
|
||||
window.addEventListener("blur", handleBlur);
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
|
||||
// 设置清理函数
|
||||
// Set cleanup function
|
||||
cleanupFn = () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside, true);
|
||||
document.removeEventListener("contextmenu", handleRightClick);
|
||||
@@ -183,7 +183,7 @@ export function FileManagerContextMenu({
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
window.removeEventListener("scroll", handleScroll, true);
|
||||
};
|
||||
}, 50); // 50ms延迟,确保不会捕获到触发菜单的点击
|
||||
}, 50); // 50ms delay to ensure we don't capture the click that triggered the menu
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -204,13 +204,13 @@ export function FileManagerContextMenu({
|
||||
(f) => f.type === "file" && f.executable,
|
||||
);
|
||||
|
||||
// 构建菜单项
|
||||
// Build menu items
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if (isFileContext) {
|
||||
// 文件/文件夹选中时的菜单
|
||||
// Menu when files/folders are selected
|
||||
|
||||
// 打开终端功能 - 支持文件和文件夹
|
||||
// Open terminal function - supports files and folders
|
||||
if (onOpenTerminal) {
|
||||
const targetPath = isSingleFile
|
||||
? files[0].type === "directory"
|
||||
@@ -229,7 +229,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 运行可执行文件功能 - 仅对单个可执行文件显示
|
||||
// Run executable file function - only show for single executable files
|
||||
if (isSingleFile && hasExecutableFiles && onRunExecutable) {
|
||||
menuItems.push({
|
||||
icon: <Play className="w-4 h-4" />,
|
||||
@@ -239,7 +239,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 添加分隔符(如果有上述功能)
|
||||
// Add separator (if above functions exist)
|
||||
if (
|
||||
onOpenTerminal ||
|
||||
(isSingleFile && hasExecutableFiles && onRunExecutable)
|
||||
@@ -247,7 +247,7 @@ export function FileManagerContextMenu({
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// 预览功能
|
||||
// Preview function
|
||||
if (hasFiles && onPreview) {
|
||||
menuItems.push({
|
||||
icon: <Eye className="w-4 h-4" />,
|
||||
@@ -257,7 +257,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 下载功能
|
||||
// Download function
|
||||
if (hasFiles && onDownload) {
|
||||
menuItems.push({
|
||||
icon: <Download className="w-4 h-4" />,
|
||||
@@ -269,7 +269,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 拖拽到桌面菜单项(支持浏览器和桌面应用)
|
||||
// Drag to desktop menu item (supports browser and desktop apps)
|
||||
if (hasFiles && onDragToDesktop) {
|
||||
const isModernBrowser = "showSaveFilePicker" in window;
|
||||
menuItems.push({
|
||||
@@ -284,7 +284,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// PIN/UNPIN 功能 - 仅对单个文件显示
|
||||
// PIN/UNPIN function - only show for single files
|
||||
if (isSingleFile && files[0].type === "file") {
|
||||
const isCurrentlyPinned = isPinned ? isPinned(files[0]) : false;
|
||||
|
||||
@@ -303,7 +303,7 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文件夹快捷方式 - 仅对单个文件夹显示
|
||||
// Add folder shortcut - only show for single folders
|
||||
if (isSingleFile && files[0].type === "directory" && onAddShortcut) {
|
||||
menuItems.push({
|
||||
icon: <Bookmark className="w-4 h-4" />,
|
||||
@@ -312,7 +312,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 添加分隔符(如果有上述功能)
|
||||
// Add separator (if above functions exist)
|
||||
if (
|
||||
(hasFiles && (onPreview || onDownload || onDragToDesktop)) ||
|
||||
(isSingleFile &&
|
||||
@@ -323,7 +323,7 @@ export function FileManagerContextMenu({
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// 重命名功能
|
||||
// Rename function
|
||||
if (isSingleFile && onRename) {
|
||||
menuItems.push({
|
||||
icon: <Edit3 className="w-4 h-4" />,
|
||||
@@ -333,7 +333,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 复制功能
|
||||
// Copy function
|
||||
if (onCopy) {
|
||||
menuItems.push({
|
||||
icon: <Copy className="w-4 h-4" />,
|
||||
@@ -345,7 +345,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 剪切功能
|
||||
// Cut function
|
||||
if (onCut) {
|
||||
menuItems.push({
|
||||
icon: <Scissors className="w-4 h-4" />,
|
||||
@@ -357,12 +357,12 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 添加分隔符(如果有编辑功能)
|
||||
// Add separator (if edit functions exist)
|
||||
if ((isSingleFile && onRename) || onCopy || onCut) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// 删除功能
|
||||
// Delete function
|
||||
if (onDelete) {
|
||||
menuItems.push({
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
@@ -375,12 +375,12 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 添加分隔符(如果有删除功能)
|
||||
// Add separator (if delete function exists)
|
||||
if (onDelete) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// 属性功能
|
||||
// Properties function
|
||||
if (isSingleFile && onProperties) {
|
||||
menuItems.push({
|
||||
icon: <Info className="w-4 h-4" />,
|
||||
@@ -389,9 +389,9 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 空白区域右键菜单
|
||||
// Empty area right-click menu
|
||||
|
||||
// 在当前目录打开终端
|
||||
// Open terminal in current directory
|
||||
if (onOpenTerminal && currentPath) {
|
||||
menuItems.push({
|
||||
icon: <Terminal className="w-4 h-4" />,
|
||||
@@ -401,7 +401,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 上传功能
|
||||
// Upload function
|
||||
if (onUpload) {
|
||||
menuItems.push({
|
||||
icon: <Upload className="w-4 h-4" />,
|
||||
@@ -411,12 +411,12 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 添加分隔符(如果有终端或上传功能)
|
||||
// Add separator (if terminal or upload functions exist)
|
||||
if ((onOpenTerminal && currentPath) || onUpload) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// 新建文件夹
|
||||
// New folder
|
||||
if (onNewFolder) {
|
||||
menuItems.push({
|
||||
icon: <FolderPlus className="w-4 h-4" />,
|
||||
@@ -426,7 +426,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 新建文件
|
||||
// New file
|
||||
if (onNewFile) {
|
||||
menuItems.push({
|
||||
icon: <FilePlus className="w-4 h-4" />,
|
||||
@@ -436,12 +436,12 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 添加分隔符(如果有新建功能)
|
||||
// Add separator (if new functions exist)
|
||||
if (onNewFolder || onNewFile) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// 刷新功能
|
||||
// Refresh function
|
||||
if (onRefresh) {
|
||||
menuItems.push({
|
||||
icon: <RefreshCw className="w-4 h-4" />,
|
||||
@@ -451,7 +451,7 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// 粘贴功能
|
||||
// Paste function
|
||||
if (hasClipboard && onPaste) {
|
||||
menuItems.push({
|
||||
icon: <Clipboard className="w-4 h-4" />,
|
||||
@@ -462,15 +462,15 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤掉连续的分隔符
|
||||
// Filter out consecutive separators
|
||||
const filteredMenuItems = menuItems.filter((item, index) => {
|
||||
if (!item.separator) return true;
|
||||
|
||||
// 如果是分隔符,检查前一个和后一个是否也是分隔符
|
||||
// If it's a separator, check if previous and next are also separators
|
||||
const prevItem = index > 0 ? menuItems[index - 1] : null;
|
||||
const nextItem = index < menuItems.length - 1 ? menuItems[index + 1] : null;
|
||||
|
||||
// 如果前一个或后一个是分隔符,则过滤掉当前分隔符
|
||||
// If previous or next is a separator, filter out current separator
|
||||
if (prevItem?.separator || nextItem?.separator) {
|
||||
return false;
|
||||
}
|
||||
@@ -478,7 +478,7 @@ export function FileManagerContextMenu({
|
||||
return true;
|
||||
});
|
||||
|
||||
// 移除开头和结尾的分隔符
|
||||
// Remove separators at beginning and end
|
||||
const finalMenuItems = filteredMenuItems.filter((item, index) => {
|
||||
if (!item.separator) return true;
|
||||
return index > 0 && index < filteredMenuItems.length - 1;
|
||||
@@ -486,10 +486,10 @@ export function FileManagerContextMenu({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 透明遮罩层用于捕获点击事件 */}
|
||||
{/* Transparent overlay to capture click events */}
|
||||
<div className="fixed inset-0 z-40" />
|
||||
|
||||
{/* 菜单本体 */}
|
||||
{/* Menu body */}
|
||||
<div
|
||||
data-context-menu
|
||||
className="fixed bg-dark-bg border border-dark-border rounded-lg shadow-xl min-w-[180px] max-w-[250px] z-50 overflow-hidden"
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { FileItem } from "../../../types/index.js";
|
||||
|
||||
// Linus式数据结构:创建意图与实际文件分离
|
||||
// Linus-style data structure: separate creation intent from actual files
|
||||
interface CreateIntent {
|
||||
id: string;
|
||||
type: 'file' | 'directory';
|
||||
@@ -33,12 +33,12 @@ interface CreateIntent {
|
||||
currentName: string;
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
// Format file size
|
||||
function formatFileSize(bytes?: number): string {
|
||||
// 处理未定义或null的情况
|
||||
// Handle undefined or null cases
|
||||
if (bytes === undefined || bytes === null) return "-";
|
||||
|
||||
// 0字节的文件显示为 "0 B"
|
||||
// Display 0-byte files as "0 B"
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
@@ -50,7 +50,7 @@ function formatFileSize(bytes?: number): string {
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
// 对于小于10的数值显示一位小数,大于10的显示整数
|
||||
// Display one decimal place for values less than 10, integers for values greater than 10
|
||||
const formattedSize =
|
||||
size < 10 && unitIndex > 0 ? size.toFixed(1) : Math.round(size).toString();
|
||||
|
||||
@@ -93,7 +93,7 @@ interface FileManagerGridProps {
|
||||
onSystemDragStart?: (files: FileItem[]) => void;
|
||||
onSystemDragEnd?: (e: DragEvent) => void;
|
||||
hasClipboard?: boolean;
|
||||
// Linus式创建意图props
|
||||
// Linus-style creation intent props
|
||||
createIntent?: CreateIntent | null;
|
||||
onConfirmCreate?: (name: string) => void;
|
||||
onCancelCreate?: () => void;
|
||||
@@ -204,14 +204,14 @@ export function FileManagerGrid({
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
|
||||
// 统一拖拽状态管理
|
||||
// Unified drag state management
|
||||
const [dragState, setDragState] = useState<DragState>({
|
||||
type: "none",
|
||||
files: [],
|
||||
counter: 0,
|
||||
});
|
||||
|
||||
// 全局鼠标移动监听 - 用于拖拽tooltip跟随
|
||||
// Global mouse move listener - for drag tooltip following
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseMove = (e: MouseEvent) => {
|
||||
if (dragState.type === "internal" && dragState.files.length > 0) {
|
||||
@@ -231,11 +231,11 @@ export function FileManagerGrid({
|
||||
|
||||
const editInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 开始编辑时设置初始名称
|
||||
// Set initial name when starting edit
|
||||
useEffect(() => {
|
||||
if (editingFile) {
|
||||
setEditingName(editingFile.name);
|
||||
// 延迟聚焦以确保DOM已更新
|
||||
// Delay focus to ensure DOM is updated
|
||||
setTimeout(() => {
|
||||
editInputRef.current?.focus();
|
||||
editInputRef.current?.select();
|
||||
@@ -243,7 +243,7 @@ export function FileManagerGrid({
|
||||
}
|
||||
}, [editingFile]);
|
||||
|
||||
// 处理编辑确认
|
||||
// Handle edit confirmation
|
||||
const handleEditConfirm = () => {
|
||||
if (
|
||||
editingFile &&
|
||||
@@ -256,13 +256,13 @@ export function FileManagerGrid({
|
||||
onCancelEdit?.();
|
||||
};
|
||||
|
||||
// 处理编辑取消
|
||||
// Handle edit cancellation
|
||||
const handleEditCancel = () => {
|
||||
setEditingName("");
|
||||
onCancelEdit?.();
|
||||
};
|
||||
|
||||
// 处理输入框按键
|
||||
// Handle input key events
|
||||
const handleEditKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -273,9 +273,9 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// 文件拖拽处理函数
|
||||
// File drag handling function
|
||||
const handleFileDragStart = (e: React.DragEvent, file: FileItem) => {
|
||||
// 如果拖拽的文件已选中,则拖拽所有选中的文件
|
||||
// If dragged file is selected, drag all selected files
|
||||
const filesToDrag = selectedFiles.includes(file) ? selectedFiles : [file];
|
||||
|
||||
setDragState({
|
||||
@@ -285,14 +285,14 @@ export function FileManagerGrid({
|
||||
mousePosition: { x: e.clientX, y: e.clientY },
|
||||
});
|
||||
|
||||
// 设置拖拽数据,添加内部拖拽标识
|
||||
// Set drag data, add internal drag identifier
|
||||
const dragData = {
|
||||
type: "internal_files",
|
||||
files: filesToDrag.map((f) => f.path),
|
||||
};
|
||||
e.dataTransfer.setData("text/plain", JSON.stringify(dragData));
|
||||
|
||||
// 触发系统级拖拽开始
|
||||
// Trigger system-level drag start
|
||||
onSystemDragStart?.(filesToDrag);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
};
|
||||
@@ -301,7 +301,7 @@ export function FileManagerGrid({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 只有拖拽到不同文件且不是被拖拽的文件时才设置目标
|
||||
// Only set target when dragging to different file and not being dragged file
|
||||
if (
|
||||
dragState.type === "internal" &&
|
||||
!dragState.files.some((f) => f.path === targetFile.path)
|
||||
@@ -315,7 +315,7 @@ export function FileManagerGrid({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 清除拖拽目标高亮
|
||||
// Clear drag target highlight
|
||||
if (dragState.target?.path === targetFile.path) {
|
||||
setDragState((prev) => ({ ...prev, target: undefined }));
|
||||
}
|
||||
@@ -330,7 +330,7 @@ export function FileManagerGrid({
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否拖拽到自身
|
||||
// Check if dragging to self
|
||||
const isDroppingOnSelf = dragState.files.some(
|
||||
(f) => f.path === targetFile.path,
|
||||
);
|
||||
@@ -340,13 +340,13 @@ export function FileManagerGrid({
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断拖拽行为:
|
||||
// 1. 文件/文件夹 拖拽到 文件夹 = 移动操作
|
||||
// 2. 单个文件 拖拽到 单个文件 = diff对比
|
||||
// 3. 其他情况 = 无效操作
|
||||
// Determine drag behavior:
|
||||
// 1. File/folder drag to folder = move operation
|
||||
// 2. Single file drag to single file = diff comparison
|
||||
// 3. Other cases = invalid operation
|
||||
|
||||
if (targetFile.type === "directory") {
|
||||
// 移动操作
|
||||
// Move operation
|
||||
console.log(
|
||||
"Moving files to directory:",
|
||||
dragState.files.map((f) => f.name),
|
||||
@@ -359,7 +359,7 @@ export function FileManagerGrid({
|
||||
dragState.files.length === 1 &&
|
||||
dragState.files[0].type === "file"
|
||||
) {
|
||||
// diff对比操作
|
||||
// Diff comparison operation
|
||||
console.log(
|
||||
"Comparing files:",
|
||||
dragState.files[0].name,
|
||||
@@ -368,7 +368,7 @@ export function FileManagerGrid({
|
||||
);
|
||||
onFileDiff?.(dragState.files[0], targetFile);
|
||||
} else {
|
||||
// 无效操作,给用户提示
|
||||
// Invalid operation, notify user
|
||||
console.log("Invalid drag operation");
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ export function FileManagerGrid({
|
||||
const handleFileDragEnd = (e: React.DragEvent) => {
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
|
||||
// 触发系统级拖拽结束检测
|
||||
// Trigger system-level drag end detection
|
||||
onSystemDragEnd?.(e.nativeEvent);
|
||||
};
|
||||
|
||||
@@ -395,17 +395,17 @@ export function FileManagerGrid({
|
||||
} | null>(null);
|
||||
const [justFinishedSelecting, setJustFinishedSelecting] = useState(false);
|
||||
|
||||
// 导航历史管理
|
||||
// Navigation history management
|
||||
const [navigationHistory, setNavigationHistory] = useState<string[]>([
|
||||
currentPath,
|
||||
]);
|
||||
const [historyIndex, setHistoryIndex] = useState(0);
|
||||
|
||||
// 路径编辑状态
|
||||
// Path editing state
|
||||
const [isEditingPath, setIsEditingPath] = useState(false);
|
||||
const [editPathValue, setEditPathValue] = useState(currentPath);
|
||||
|
||||
// 更新导航历史
|
||||
// Update navigation history
|
||||
useEffect(() => {
|
||||
const lastPath = navigationHistory[historyIndex];
|
||||
if (currentPath !== lastPath) {
|
||||
@@ -416,7 +416,7 @@ export function FileManagerGrid({
|
||||
}
|
||||
}, [currentPath]);
|
||||
|
||||
// 导航函数
|
||||
// Navigation functions
|
||||
const goBack = () => {
|
||||
if (historyIndex > 0) {
|
||||
const newIndex = historyIndex - 1;
|
||||
@@ -444,7 +444,7 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// 路径导航
|
||||
// Path navigation
|
||||
const pathParts = currentPath.split("/").filter(Boolean);
|
||||
const navigateToPath = (index: number) => {
|
||||
if (index === -1) {
|
||||
@@ -455,7 +455,7 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// 路径编辑功能
|
||||
// Path editing functionality
|
||||
const startEditingPath = () => {
|
||||
setEditPathValue(currentPath);
|
||||
setIsEditingPath(true);
|
||||
@@ -469,7 +469,7 @@ export function FileManagerGrid({
|
||||
const confirmEditingPath = () => {
|
||||
const trimmedPath = editPathValue.trim();
|
||||
if (trimmedPath) {
|
||||
// 确保路径以 / 开头
|
||||
// Ensure path starts with /
|
||||
const normalizedPath = trimmedPath.startsWith("/")
|
||||
? trimmedPath
|
||||
: "/" + trimmedPath;
|
||||
@@ -488,24 +488,24 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// 同步editPathValue与currentPath
|
||||
// Sync editPathValue with currentPath
|
||||
useEffect(() => {
|
||||
if (!isEditingPath) {
|
||||
setEditPathValue(currentPath);
|
||||
}
|
||||
}, [currentPath, isEditingPath]);
|
||||
|
||||
// 拖放处理 - 区分内部文件拖拽和外部文件上传
|
||||
// Drag and drop handling - distinguish internal file drag and external file upload
|
||||
const handleDragEnter = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 检查是否是内部文件拖拽
|
||||
// Check if it's internal file drag
|
||||
const isInternalDrag = dragState.type === "internal";
|
||||
|
||||
if (!isInternalDrag) {
|
||||
// 只有外部文件拖拽才显示上传提示
|
||||
// Only show upload prompt for external file drag
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
type: "external",
|
||||
@@ -524,7 +524,7 @@ export function FileManagerGrid({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 检查是否是内部文件拖拽
|
||||
// Check if it's internal file drag
|
||||
const isInternalDrag = dragState.type === "internal";
|
||||
|
||||
if (!isInternalDrag && dragState.type === "external") {
|
||||
@@ -546,11 +546,11 @@ export function FileManagerGrid({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 检查是否是内部文件拖拽
|
||||
// Check if it's internal file drag
|
||||
const isInternalDrag = dragState.type === "internal";
|
||||
|
||||
if (isInternalDrag) {
|
||||
// 更新鼠标位置
|
||||
// Update mouse position
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
mousePosition: { x: e.clientX, y: e.clientY },
|
||||
@@ -563,15 +563,15 @@ export function FileManagerGrid({
|
||||
[dragState.type],
|
||||
);
|
||||
|
||||
// 滚轮事件处理,确保滚动正常工作
|
||||
// Mouse wheel event handling, ensure scrolling works normally
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
// 不阻止默认滚动行为,让浏览器自己处理滚动
|
||||
// Don't prevent default scroll behavior, let browser handle scrolling
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
// 框选功能实现
|
||||
// Box selection functionality implementation
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
// 只在空白区域开始框选,避免干扰文件点击
|
||||
// Only start box selection in empty area, avoid interfering with file clicks
|
||||
if (e.target === e.currentTarget && e.button === 0) {
|
||||
e.preventDefault();
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
@@ -582,7 +582,7 @@ export function FileManagerGrid({
|
||||
setSelectionStart({ x: startX, y: startY });
|
||||
setSelectionRect({ x: startX, y: startY, width: 0, height: 0 });
|
||||
|
||||
// 重置刚完成框选的标志,准备新的框选
|
||||
// Reset flag for just completed selection, prepare for new selection
|
||||
setJustFinishedSelecting(false);
|
||||
}
|
||||
}, []);
|
||||
@@ -601,7 +601,7 @@ export function FileManagerGrid({
|
||||
|
||||
setSelectionRect({ x, y, width, height });
|
||||
|
||||
// 检测与文件项的交集,进行实时选择
|
||||
// Detect intersection with file items, perform real-time selection
|
||||
if (gridRef.current) {
|
||||
const fileElements =
|
||||
gridRef.current.querySelectorAll("[data-file-path]");
|
||||
@@ -611,7 +611,7 @@ export function FileManagerGrid({
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const containerRect = gridRef.current!.getBoundingClientRect();
|
||||
|
||||
// 简化坐标计算 - 直接使用相对于容器的坐标
|
||||
// Simplify coordinate calculation - directly use coordinates relative to container
|
||||
const relativeElementRect = {
|
||||
left: elementRect.left - containerRect.left,
|
||||
top: elementRect.top - containerRect.top,
|
||||
@@ -619,7 +619,7 @@ export function FileManagerGrid({
|
||||
bottom: elementRect.bottom - containerRect.top,
|
||||
};
|
||||
|
||||
// 选择框坐标
|
||||
// Selection box coordinates
|
||||
const selectionBox = {
|
||||
left: x,
|
||||
top: y,
|
||||
@@ -627,7 +627,7 @@ export function FileManagerGrid({
|
||||
bottom: y + height,
|
||||
};
|
||||
|
||||
// 检查是否相交
|
||||
// Check if intersecting
|
||||
const intersects = !(
|
||||
relativeElementRect.right < selectionBox.left ||
|
||||
relativeElementRect.left > selectionBox.right ||
|
||||
@@ -646,7 +646,7 @@ export function FileManagerGrid({
|
||||
|
||||
console.log("Total selected paths:", selectedPaths.length);
|
||||
|
||||
// 更新选中的文件
|
||||
// Update selected files
|
||||
const newSelection = files.filter((file) =>
|
||||
selectedPaths.includes(file.path),
|
||||
);
|
||||
@@ -668,7 +668,7 @@ export function FileManagerGrid({
|
||||
setSelectionStart(null);
|
||||
setSelectionRect(null);
|
||||
|
||||
// 只有当移动距离足够大时才认为是框选,否则是点击
|
||||
// Only consider as box selection when movement distance is large enough, otherwise it's a click
|
||||
const startPos = selectionStart;
|
||||
if (startPos) {
|
||||
const rect = gridRef.current?.getBoundingClientRect();
|
||||
@@ -680,13 +680,13 @@ export function FileManagerGrid({
|
||||
);
|
||||
|
||||
if (distance > 5) {
|
||||
// 真正的框选,设置标志防止立即清空
|
||||
// Real box selection, set flag to prevent immediate clearing
|
||||
setJustFinishedSelecting(true);
|
||||
setTimeout(() => {
|
||||
setJustFinishedSelecting(false);
|
||||
}, 50);
|
||||
} else {
|
||||
// 只是点击,不设置标志,让handleGridClick正常处理
|
||||
// Just a click, don't set flag, let handleGridClick handle normally
|
||||
setJustFinishedSelecting(false);
|
||||
}
|
||||
}
|
||||
@@ -696,7 +696,7 @@ export function FileManagerGrid({
|
||||
[isSelecting, selectionStart],
|
||||
);
|
||||
|
||||
// 全局鼠标事件监听,确保在容器外也能结束框选
|
||||
// Global mouse event listener, ensure box selection can end outside container
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseUp = (e: MouseEvent) => {
|
||||
if (isSelecting) {
|
||||
@@ -704,7 +704,7 @@ export function FileManagerGrid({
|
||||
setSelectionStart(null);
|
||||
setSelectionRect(null);
|
||||
|
||||
// 全局mouseup说明是拖拽框选,设置标志
|
||||
// Global mouseup indicates drag box selection, set flag
|
||||
setJustFinishedSelecting(true);
|
||||
setTimeout(() => {
|
||||
setJustFinishedSelecting(false);
|
||||
@@ -744,7 +744,7 @@ export function FileManagerGrid({
|
||||
e.stopPropagation();
|
||||
|
||||
if (dragState.type === "internal") {
|
||||
// 内部拖拽到空白区域:触发下载
|
||||
// Internal drag to empty area: trigger download
|
||||
console.log(
|
||||
"Internal drag to empty area detected, triggering download",
|
||||
);
|
||||
@@ -752,23 +752,23 @@ export function FileManagerGrid({
|
||||
onDownload(dragState.files);
|
||||
}
|
||||
} else if (dragState.type === "external") {
|
||||
// 外部拖拽:处理文件上传
|
||||
// External drag: handle file upload
|
||||
if (onUpload && e.dataTransfer.files.length > 0) {
|
||||
onUpload(e.dataTransfer.files);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置拖拽状态
|
||||
// Reset drag state
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
},
|
||||
[onUpload, onDownload, dragState],
|
||||
);
|
||||
|
||||
// 文件选择处理
|
||||
// File selection handling
|
||||
const handleFileClick = (file: FileItem, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
// 确保网格获得焦点以支持键盘事件
|
||||
// Ensure grid gets focus to support keyboard events
|
||||
if (gridRef.current) {
|
||||
gridRef.current.focus();
|
||||
}
|
||||
@@ -781,11 +781,11 @@ export function FileManagerGrid({
|
||||
);
|
||||
|
||||
if (event.detail === 2) {
|
||||
// 双击打开
|
||||
// Double click to open
|
||||
console.log("Double click - opening file");
|
||||
onFileOpen(file);
|
||||
} else {
|
||||
// 单击选择
|
||||
// Single click to select
|
||||
const multiSelect = event.ctrlKey || event.metaKey;
|
||||
const rangeSelect = event.shiftKey;
|
||||
|
||||
@@ -797,7 +797,7 @@ export function FileManagerGrid({
|
||||
);
|
||||
|
||||
if (rangeSelect && selectedFiles.length > 0) {
|
||||
// 范围选择 (Shift+点击)
|
||||
// Range selection (Shift+click)
|
||||
console.log("Range selection");
|
||||
const lastSelected = selectedFiles[selectedFiles.length - 1];
|
||||
const currentIndex = files.findIndex((f) => f.path === file.path);
|
||||
@@ -811,7 +811,7 @@ export function FileManagerGrid({
|
||||
onSelectionChange(rangeFiles);
|
||||
}
|
||||
} else if (multiSelect) {
|
||||
// 多选 (Ctrl+点击)
|
||||
// Multi-selection (Ctrl+click)
|
||||
console.log("Multi selection");
|
||||
const isSelected = selectedFiles.some((f) => f.path === file.path);
|
||||
if (isSelected) {
|
||||
@@ -822,21 +822,21 @@ export function FileManagerGrid({
|
||||
onSelectionChange([...selectedFiles, file]);
|
||||
}
|
||||
} else {
|
||||
// 单选
|
||||
// Single selection
|
||||
console.log("Single selection - should select only:", file.name);
|
||||
onSelectionChange([file]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 空白区域点击取消选择
|
||||
// Click empty area to cancel selection
|
||||
const handleGridClick = (event: React.MouseEvent) => {
|
||||
// 确保网格获得焦点以支持键盘事件
|
||||
// Ensure grid gets focus to support keyboard events
|
||||
if (gridRef.current) {
|
||||
gridRef.current.focus();
|
||||
}
|
||||
|
||||
// 如果刚完成框选,不要清空选择
|
||||
// If just completed box selection, don't clear selection
|
||||
if (
|
||||
event.target === event.currentTarget &&
|
||||
!isSelecting &&
|
||||
@@ -846,10 +846,10 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// 键盘支持
|
||||
// Keyboard support
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// 检查是否有输入框或可编辑元素获得焦点,如果有则跳过
|
||||
// Check if input box or editable element has focus, skip if so
|
||||
const activeElement = document.activeElement;
|
||||
if (
|
||||
activeElement &&
|
||||
@@ -910,7 +910,7 @@ export function FileManagerGrid({
|
||||
break;
|
||||
case "Delete":
|
||||
if (selectedFiles.length > 0 && onDelete) {
|
||||
// 触发删除操作
|
||||
// Trigger delete operation
|
||||
onDelete(selectedFiles);
|
||||
}
|
||||
break;
|
||||
@@ -954,9 +954,9 @@ export function FileManagerGrid({
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-dark-bg overflow-hidden">
|
||||
{/* 工具栏和路径导航 */}
|
||||
{/* Toolbar and path navigation */}
|
||||
<div className="flex-shrink-0 border-b border-dark-border">
|
||||
{/* 导航按钮 */}
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex items-center gap-1 p-2 border-b border-dark-border">
|
||||
<button
|
||||
onClick={goBack}
|
||||
@@ -1001,10 +1001,10 @@ export function FileManagerGrid({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 面包屑导航 */}
|
||||
{/* Breadcrumb navigation */}
|
||||
<div className="flex items-center px-3 py-2 text-sm">
|
||||
{isEditingPath ? (
|
||||
// 编辑模式:路径输入框
|
||||
// Edit mode: path input box
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -1035,7 +1035,7 @@ export function FileManagerGrid({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
// 查看模式:面包屑导航
|
||||
// View mode: breadcrumb navigation
|
||||
<>
|
||||
<button
|
||||
onClick={() => navigateToPath(-1)}
|
||||
@@ -1068,7 +1068,7 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主文件网格 - 滚动区域 */}
|
||||
{/* Main file grid - scroll area */}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
<div
|
||||
ref={gridRef}
|
||||
@@ -1089,7 +1089,7 @@ export function FileManagerGrid({
|
||||
onContextMenu={(e) => onContextMenu?.(e)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{/* 拖拽提示覆盖层 */}
|
||||
{/* Drag hint overlay */}
|
||||
{dragState.type === "external" && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-10 pointer-events-none animate-in fade-in-0">
|
||||
<div className="text-center p-8 bg-background/95 border-2 border-dashed border-primary rounded-lg shadow-lg">
|
||||
@@ -1125,7 +1125,7 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
|
||||
{/* Linus式创建意图UI - 纯粹分离 */}
|
||||
{/* Linus-style creation intent UI - pure separation */}
|
||||
{createIntent && (
|
||||
<CreateIntentGridItem
|
||||
intent={createIntent}
|
||||
@@ -1138,7 +1138,7 @@ export function FileManagerGrid({
|
||||
(f) => f.path === file.path,
|
||||
);
|
||||
|
||||
// 详细调试路径比较
|
||||
// Detailed debug path comparison
|
||||
if (selectedFiles.length > 0) {
|
||||
console.log(`\n=== File: ${file.name} ===`);
|
||||
console.log(`File path: "${file.path}"`);
|
||||
@@ -1184,10 +1184,10 @@ export function FileManagerGrid({
|
||||
onDragEnd={handleFileDragEnd}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{/* 文件图标 */}
|
||||
{/* File icon */}
|
||||
<div className="mb-2">{getFileIcon(file, viewMode)}</div>
|
||||
|
||||
{/* 文件名 */}
|
||||
{/* File name */}
|
||||
<div className="w-full flex flex-col items-center">
|
||||
{editingFile?.path === file.path ? (
|
||||
<input
|
||||
@@ -1207,9 +1207,9 @@ export function FileManagerGrid({
|
||||
) : (
|
||||
<p
|
||||
className="text-xs text-foreground truncate cursor-pointer hover:bg-accent px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full text-center"
|
||||
title={`${file.name} (点击重命名)`}
|
||||
title={`${file.name} (click to rename)`}
|
||||
onClick={(e) => {
|
||||
// 阻止文件选择事件
|
||||
// Prevent file selection event
|
||||
if (onStartEdit) {
|
||||
e.stopPropagation();
|
||||
onStartEdit(file);
|
||||
@@ -1241,9 +1241,9 @@ export function FileManagerGrid({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
/* 列表视图 */
|
||||
/* List view */
|
||||
<div className="space-y-1">
|
||||
{/* Linus式创建意图UI - 列表视图 */}
|
||||
{/* Linus-style creation intent UI - list view */}
|
||||
{createIntent && (
|
||||
<CreateIntentListItem
|
||||
intent={createIntent}
|
||||
@@ -1282,12 +1282,12 @@ export function FileManagerGrid({
|
||||
onDrop={(e) => handleFileDrop(e, file)}
|
||||
onDragEnd={handleFileDragEnd}
|
||||
>
|
||||
{/* 文件图标 */}
|
||||
{/* File icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{getFileIcon(file, viewMode)}
|
||||
</div>
|
||||
|
||||
{/* 文件信息 */}
|
||||
{/* File info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{editingFile?.path === file.path ? (
|
||||
<input
|
||||
@@ -1307,7 +1307,7 @@ export function FileManagerGrid({
|
||||
) : (
|
||||
<p
|
||||
className="text-sm text-foreground truncate cursor-pointer hover:bg-accent px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full"
|
||||
title={`${file.name} (点击重命名)`}
|
||||
title={`${file.name} (click to rename)`}
|
||||
onClick={(e) => {
|
||||
// 阻止文件选择事件
|
||||
if (onStartEdit) {
|
||||
@@ -1334,7 +1334,7 @@ export function FileManagerGrid({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 文件大小 */}
|
||||
{/* File size */}
|
||||
<div className="flex-shrink-0 text-right">
|
||||
{file.type === "file" &&
|
||||
file.size !== undefined &&
|
||||
@@ -1345,7 +1345,7 @@ export function FileManagerGrid({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 权限信息 */}
|
||||
{/* Permission info */}
|
||||
<div className="flex-shrink-0 text-right w-20">
|
||||
{file.permissions && (
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
@@ -1359,7 +1359,7 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 框选矩形 */}
|
||||
{/* Selection rectangle */}
|
||||
{isSelecting && selectionRect && (
|
||||
<div
|
||||
className="absolute pointer-events-none border-2 border-primary bg-primary/10 z-50"
|
||||
@@ -1374,7 +1374,7 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 状态栏 */}
|
||||
{/* Status bar */}
|
||||
<div className="flex-shrink-0 border-t border-dark-border px-4 py-2 text-xs text-muted-foreground">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>{t("fileManager.itemCount", { count: files.length })}</span>
|
||||
@@ -1386,7 +1386,7 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 拖拽跟随tooltip */}
|
||||
{/* Drag following tooltip */}
|
||||
{dragState.type === "internal" &&
|
||||
dragState.files.length > 0 &&
|
||||
dragState.mousePosition && (
|
||||
@@ -1403,14 +1403,14 @@ export function FileManagerGrid({
|
||||
<>
|
||||
<Move className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
移动到 {dragState.target.name}
|
||||
Move to {dragState.target.name}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GitCompare className="w-4 h-4 text-purple-500" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
与 {dragState.target.name} 进行diff对比
|
||||
Diff compare with {dragState.target.name}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
@@ -1418,7 +1418,7 @@ export function FileManagerGrid({
|
||||
<>
|
||||
<Download className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
拖到窗口外下载 ({dragState.files.length} 个文件)
|
||||
Drag outside window to download ({dragState.files.length} files)
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
@@ -1429,7 +1429,7 @@ export function FileManagerGrid({
|
||||
);
|
||||
}
|
||||
|
||||
// Linus式创建意图组件:Grid视图
|
||||
// Linus-style creation intent component: Grid view
|
||||
function CreateIntentGridItem({
|
||||
intent,
|
||||
onConfirm,
|
||||
@@ -1482,7 +1482,7 @@ function CreateIntentGridItem({
|
||||
);
|
||||
}
|
||||
|
||||
// Linus式创建意图组件:List视图
|
||||
// Linus-style creation intent component: List view
|
||||
function CreateIntentListItem({
|
||||
intent,
|
||||
onConfirm,
|
||||
|
||||
@@ -52,7 +52,7 @@ interface FileManagerModernProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
// Linus式数据结构:创建意图与实际文件完全分离
|
||||
// Linus-style data structure: creation intent completely separated from actual files
|
||||
interface CreateIntent {
|
||||
id: string;
|
||||
type: 'file' | 'directory';
|
||||
@@ -60,7 +60,7 @@ interface CreateIntent {
|
||||
currentName: string;
|
||||
}
|
||||
|
||||
// 内部组件,使用窗口管理器
|
||||
// Internal component, uses window manager
|
||||
function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
const { openWindow } = useWindowManager();
|
||||
const { t } = useTranslation();
|
||||
@@ -94,13 +94,13 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
files: [],
|
||||
});
|
||||
|
||||
// 操作状态
|
||||
// Operation state
|
||||
const [clipboard, setClipboard] = useState<{
|
||||
files: FileItem[];
|
||||
operation: "copy" | "cut";
|
||||
} | null>(null);
|
||||
|
||||
// 撤销历史
|
||||
// Undo history
|
||||
interface UndoAction {
|
||||
type: "copy" | "cut" | "delete";
|
||||
description: string;
|
||||
@@ -119,7 +119,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
|
||||
const [undoHistory, setUndoHistory] = useState<UndoAction[]>([]);
|
||||
|
||||
// Linus式状态:创建意图与文件编辑分离
|
||||
// Linus-style state: creation intent separated from file editing
|
||||
const [createIntent, setCreateIntent] = useState<CreateIntent | null>(null);
|
||||
const [editingFile, setEditingFile] = useState<FileItem | null>(null);
|
||||
|
||||
@@ -133,43 +133,43 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
maxFileSize: 5120, // 5GB - support large files like SSH tools should
|
||||
});
|
||||
|
||||
// 拖拽到桌面功能
|
||||
// Drag to desktop functionality
|
||||
const dragToDesktop = useDragToDesktop({
|
||||
sshSessionId: sshSessionId || "",
|
||||
sshHost: currentHost!,
|
||||
});
|
||||
|
||||
// 系统级拖拽到桌面功能(新方案)
|
||||
// System-level drag to desktop functionality (new approach)
|
||||
const systemDrag = useDragToSystemDesktop({
|
||||
sshSessionId: sshSessionId || "",
|
||||
sshHost: currentHost!,
|
||||
});
|
||||
|
||||
// 初始化SSH连接
|
||||
// Initialize SSH connection
|
||||
useEffect(() => {
|
||||
if (currentHost) {
|
||||
initializeSSHConnection();
|
||||
}
|
||||
}, [currentHost]);
|
||||
|
||||
// 文件列表更新
|
||||
// File list update
|
||||
useEffect(() => {
|
||||
if (sshSessionId) {
|
||||
handleRefreshDirectory();
|
||||
}
|
||||
}, [sshSessionId, currentPath]);
|
||||
|
||||
// 文件拖拽到外部处理
|
||||
// Handle file drag to external
|
||||
const handleFileDragStart = useCallback(
|
||||
(files: FileItem[]) => {
|
||||
// 记录当前拖拽的文件
|
||||
// Record currently dragged files
|
||||
systemDrag.startDragToSystem(files, {
|
||||
enableToast: true,
|
||||
onSuccess: () => {
|
||||
clearSelection();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("拖拽失败:", error);
|
||||
console.error("Drag failed:", error);
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -178,7 +178,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
|
||||
const handleFileDragEnd = useCallback(
|
||||
(e: DragEvent) => {
|
||||
// 检查是否拖拽到窗口外
|
||||
// Check if dragged outside window
|
||||
const margin = 50;
|
||||
const isOutside =
|
||||
e.clientX < margin ||
|
||||
@@ -187,12 +187,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
e.clientY > window.innerHeight - margin;
|
||||
|
||||
if (isOutside) {
|
||||
// 延迟执行,避免与其他事件冲突
|
||||
// Delay execution to avoid conflicts with other events
|
||||
setTimeout(() => {
|
||||
systemDrag.handleDragEnd(e);
|
||||
}, 100);
|
||||
} else {
|
||||
// 取消拖拽
|
||||
// Cancel drag
|
||||
systemDrag.cancelDragToSystem();
|
||||
}
|
||||
},
|
||||
@@ -279,10 +279,10 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// 防抖刷新函数 - 防止疯狂点击
|
||||
// Debounced refresh function - prevent excessive clicking
|
||||
function handleRefreshDirectory() {
|
||||
const now = Date.now();
|
||||
const DEBOUNCE_MS = 500; // 500ms防抖
|
||||
const DEBOUNCE_MS = 500; // 500ms debounce
|
||||
|
||||
if (now - lastRefreshTime < DEBOUNCE_MS) {
|
||||
console.log("Refresh ignored - too frequent");
|
||||
@@ -311,12 +311,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
// 确保SSH连接有效
|
||||
await ensureSSHConnection();
|
||||
|
||||
// 读取文件内容
|
||||
// Read file content
|
||||
const fileContent = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onerror = () => reject(reader.error);
|
||||
|
||||
// 检查文件类型,决定读取方式
|
||||
// Check file type to determine reading method
|
||||
const isTextFile =
|
||||
file.type.startsWith("text/") ||
|
||||
file.type === "application/json" ||
|
||||
@@ -390,7 +390,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
const response = await downloadSSHFile(sshSessionId, file.path);
|
||||
|
||||
if (response?.content) {
|
||||
// 转换为blob并触发下载
|
||||
// Convert to blob and trigger download
|
||||
const byteCharacters = atob(response.content);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
@@ -446,7 +446,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// 记录删除历史(虽然无法真正撤销)
|
||||
// Record deletion history (although cannot truly undo)
|
||||
const deletedFiles = files.map((file) => ({
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
@@ -454,7 +454,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
|
||||
const undoAction: UndoAction = {
|
||||
type: "delete",
|
||||
description: `删除了 ${files.length} 个项目`,
|
||||
description: t("fileManager.deletedItems", { count: files.length }),
|
||||
data: {
|
||||
operation: "cut", // Placeholder
|
||||
deletedFiles,
|
||||
@@ -484,7 +484,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Linus式创建:纯粹的意图,无副作用
|
||||
// Linus-style creation: pure intent, no side effects
|
||||
function handleCreateNewFolder() {
|
||||
const defaultName = generateUniqueName("NewFolder", "directory");
|
||||
setCreateIntent({
|
||||
@@ -543,22 +543,22 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
const symlinkInfo = await identifySSHSymlink(currentSessionId, file.path);
|
||||
|
||||
if (symlinkInfo.type === "directory") {
|
||||
// 如果软链接指向目录,导航到它
|
||||
// If symlink points to directory, navigate to it
|
||||
setCurrentPath(symlinkInfo.target);
|
||||
} else if (symlinkInfo.type === "file") {
|
||||
// 如果软链接指向文件,打开文件
|
||||
// 计算窗口位置(稍微错开)
|
||||
// If symlink points to file, open file
|
||||
// Calculate window position (slightly offset)
|
||||
const windowCount = Date.now() % 10;
|
||||
const offsetX = 120 + windowCount * 30;
|
||||
const offsetY = 120 + windowCount * 30;
|
||||
|
||||
// 创建目标文件对象
|
||||
// Create target file object
|
||||
const targetFile: FileItem = {
|
||||
...file,
|
||||
path: symlinkInfo.target,
|
||||
};
|
||||
|
||||
// 创建窗口组件工厂函数
|
||||
// Create window component factory function
|
||||
const createWindowComponent = (windowId: string) => (
|
||||
<FileWindow
|
||||
windowId={windowId}
|
||||
@@ -594,21 +594,21 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
if (file.type === "directory") {
|
||||
setCurrentPath(file.path);
|
||||
} else if (file.type === "link") {
|
||||
// 处理软链接
|
||||
// Handle symlinks
|
||||
await handleSymlinkClick(file);
|
||||
} else {
|
||||
// 在新窗口中打开文件
|
||||
// Open file in new window
|
||||
if (!sshSessionId) {
|
||||
toast.error(t("fileManager.noSSHConnection"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算窗口位置(稍微错开)
|
||||
const windowCount = Date.now() % 10; // 简单的偏移计算
|
||||
const windowCount = Date.now() % 10; // Simple offset calculation
|
||||
const offsetX = 120 + windowCount * 30;
|
||||
const offsetY = 120 + windowCount * 30;
|
||||
|
||||
const windowTitle = file.name; // 移除模式标识,由FileViewer内部控制
|
||||
const windowTitle = file.name; // Remove mode identifier, controlled internally by FileViewer
|
||||
|
||||
// 创建窗口组件工厂函数
|
||||
const createWindowComponent = (windowId: string) => (
|
||||
@@ -635,12 +635,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// 专门的文件编辑函数
|
||||
// Dedicated file editing function
|
||||
function handleFileEdit(file: FileItem) {
|
||||
handleFileOpen(file, true);
|
||||
}
|
||||
|
||||
// 专门的文件查看函数(只读)
|
||||
// Dedicated file viewing function (read-only)
|
||||
function handleFileView(file: FileItem) {
|
||||
handleFileOpen(file, false);
|
||||
}
|
||||
@@ -648,8 +648,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
function handleContextMenu(event: React.MouseEvent, file?: FileItem) {
|
||||
event.preventDefault();
|
||||
|
||||
// 如果右键点击的文件已经在选中列表中,使用所有选中的文件
|
||||
// 如果右键点击的文件不在选中列表中,只使用这一个文件
|
||||
// If right-clicked file is already in selection list, use all selected files
|
||||
// If right-clicked file is not in selection list, use only this file
|
||||
let files: FileItem[];
|
||||
if (file) {
|
||||
const isFileSelected = selectedFiles.some((f) => f.path === file.path);
|
||||
@@ -688,14 +688,14 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
|
||||
const { files, operation } = clipboard;
|
||||
|
||||
// 处理复制和剪切操作
|
||||
// Handle copy and cut operations
|
||||
let successCount = 0;
|
||||
const copiedItems: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
if (operation === "copy") {
|
||||
// 复制操作:调用复制API
|
||||
// Copy operation: call copy API
|
||||
const result = await copySSHItem(
|
||||
sshSessionId,
|
||||
file.path,
|
||||
@@ -706,7 +706,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
||||
copiedItems.push(result.uniqueName || file.name);
|
||||
successCount++;
|
||||
} else {
|
||||
// 剪切操作:移动文件到目标目录
|
||||
// Cut operation: move files to target directory
|
||||
const targetPath = currentPath.endsWith("/")
|
||||
? `${currentPath}${file.name}`
|
||||
: `${currentPath}/${file.name}`;
|
||||
|
||||
@@ -38,9 +38,9 @@ interface FileManagerSidebarProps {
|
||||
currentPath: string;
|
||||
onPathChange: (path: string) => void;
|
||||
onLoadDirectory?: (path: string) => void;
|
||||
onFileOpen?: (file: SidebarItem) => void; // 新增:处理文件打开
|
||||
onFileOpen?: (file: SidebarItem) => void; // Added: handle file opening
|
||||
sshSessionId?: string;
|
||||
refreshTrigger?: number; // 用于触发数据刷新
|
||||
refreshTrigger?: number; // Used to trigger data refresh
|
||||
}
|
||||
|
||||
export function FileManagerSidebar({
|
||||
@@ -61,7 +61,7 @@ export function FileManagerSidebar({
|
||||
new Set(["root"]),
|
||||
);
|
||||
|
||||
// 右键菜单状态
|
||||
// Right-click menu state
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -74,12 +74,12 @@ export function FileManagerSidebar({
|
||||
item: null,
|
||||
});
|
||||
|
||||
// 加载快捷功能数据
|
||||
// Load quick access data
|
||||
useEffect(() => {
|
||||
loadQuickAccessData();
|
||||
}, [currentHost, refreshTrigger]);
|
||||
|
||||
// 加载目录树(依赖sshSessionId)
|
||||
// Load directory tree (depends on sshSessionId)
|
||||
useEffect(() => {
|
||||
if (sshSessionId) {
|
||||
loadDirectoryTree();
|
||||
@@ -90,7 +90,7 @@ export function FileManagerSidebar({
|
||||
if (!currentHost?.id) return;
|
||||
|
||||
try {
|
||||
// 加载最近访问文件(限制5个)
|
||||
// Load recent files (limit to 5)
|
||||
const recentData = await getRecentFiles(currentHost.id);
|
||||
const recentItems = recentData.slice(0, 5).map((item: any) => ({
|
||||
id: `recent-${item.id}`,
|
||||
@@ -101,7 +101,7 @@ export function FileManagerSidebar({
|
||||
}));
|
||||
setRecentItems(recentItems);
|
||||
|
||||
// 加载固定文件
|
||||
// Load pinned files
|
||||
const pinnedData = await getPinnedFiles(currentHost.id);
|
||||
const pinnedItems = pinnedData.map((item: any) => ({
|
||||
id: `pinned-${item.id}`,
|
||||
@@ -111,7 +111,7 @@ export function FileManagerSidebar({
|
||||
}));
|
||||
setPinnedItems(pinnedItems);
|
||||
|
||||
// 加载文件夹快捷方式
|
||||
// Load folder shortcuts
|
||||
const shortcutData = await getFolderShortcuts(currentHost.id);
|
||||
const shortcutItems = shortcutData.map((item: any) => ({
|
||||
id: `shortcut-${item.id}`,
|
||||
@@ -122,20 +122,20 @@ export function FileManagerSidebar({
|
||||
setShortcuts(shortcutItems);
|
||||
} catch (error) {
|
||||
console.error("Failed to load quick access data:", error);
|
||||
// 如果加载失败,保持空数组
|
||||
// If loading fails, keep empty arrays
|
||||
setRecentItems([]);
|
||||
setPinnedItems([]);
|
||||
setShortcuts([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除功能实现
|
||||
// Delete functionality implementation
|
||||
const handleRemoveRecentFile = async (item: SidebarItem) => {
|
||||
if (!currentHost?.id) return;
|
||||
|
||||
try {
|
||||
await removeRecentFile(currentHost.id, item.path);
|
||||
loadQuickAccessData(); // 重新加载数据
|
||||
loadQuickAccessData(); // Reload data
|
||||
toast.success(
|
||||
t("fileManager.removedFromRecentFiles", { name: item.name }),
|
||||
);
|
||||
@@ -150,7 +150,7 @@ export function FileManagerSidebar({
|
||||
|
||||
try {
|
||||
await removePinnedFile(currentHost.id, item.path);
|
||||
loadQuickAccessData(); // 重新加载数据
|
||||
loadQuickAccessData(); // Reload data
|
||||
toast.success(t("fileManager.unpinnedSuccessfully", { name: item.name }));
|
||||
} catch (error) {
|
||||
console.error("Failed to unpin file:", error);
|
||||
@@ -163,7 +163,7 @@ export function FileManagerSidebar({
|
||||
|
||||
try {
|
||||
await removeFolderShortcut(currentHost.id, item.path);
|
||||
loadQuickAccessData(); // 重新加载数据
|
||||
loadQuickAccessData(); // Reload data
|
||||
toast.success(t("fileManager.removedShortcut", { name: item.name }));
|
||||
} catch (error) {
|
||||
console.error("Failed to remove shortcut:", error);
|
||||
@@ -175,11 +175,11 @@ export function FileManagerSidebar({
|
||||
if (!currentHost?.id || recentItems.length === 0) return;
|
||||
|
||||
try {
|
||||
// 批量删除所有recent文件
|
||||
// Batch delete all recent files
|
||||
await Promise.all(
|
||||
recentItems.map((item) => removeRecentFile(currentHost.id, item.path)),
|
||||
);
|
||||
loadQuickAccessData(); // 重新加载数据
|
||||
loadQuickAccessData(); // Reload data
|
||||
toast.success(t("fileManager.clearedAllRecentFiles"));
|
||||
} catch (error) {
|
||||
console.error("Failed to clear recent files:", error);
|
||||
@@ -187,7 +187,7 @@ export function FileManagerSidebar({
|
||||
}
|
||||
};
|
||||
|
||||
// 右键菜单处理
|
||||
// Right-click menu handling
|
||||
const handleContextMenu = (e: React.MouseEvent, item: SidebarItem) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -204,7 +204,7 @@ export function FileManagerSidebar({
|
||||
setContextMenu((prev) => ({ ...prev, isVisible: false, item: null }));
|
||||
};
|
||||
|
||||
// 点击外部关闭菜单
|
||||
// Click outside to close menu
|
||||
useEffect(() => {
|
||||
if (!contextMenu.isVisible) return;
|
||||
|
||||
@@ -223,7 +223,7 @@ export function FileManagerSidebar({
|
||||
}
|
||||
};
|
||||
|
||||
// 延迟添加监听器,避免立即触发
|
||||
// Delay adding listeners to avoid immediate trigger
|
||||
const timeoutId = setTimeout(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
@@ -240,10 +240,10 @@ export function FileManagerSidebar({
|
||||
if (!sshSessionId) return;
|
||||
|
||||
try {
|
||||
// 加载根目录
|
||||
// Load root directory
|
||||
const response = await listSSHFiles(sshSessionId, "/");
|
||||
|
||||
// listSSHFiles 现在总是返回 {files: Array, path: string} 格式
|
||||
// listSSHFiles now always returns {files: Array, path: string} format
|
||||
const rootFiles = response.files || [];
|
||||
const rootFolders = rootFiles.filter(
|
||||
(item: any) => item.type === "directory",
|
||||
@@ -255,7 +255,7 @@ export function FileManagerSidebar({
|
||||
path: folder.path,
|
||||
type: "folder" as const,
|
||||
isExpanded: false,
|
||||
children: [], // 子目录将按需加载
|
||||
children: [], // Subdirectories will be loaded on demand
|
||||
}));
|
||||
|
||||
setDirectoryTree([
|
||||
@@ -270,7 +270,7 @@ export function FileManagerSidebar({
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Failed to load directory tree:", error);
|
||||
// 如果加载失败,显示简单的根目录
|
||||
// If loading fails, show simple root directory
|
||||
setDirectoryTree([
|
||||
{
|
||||
id: "root",
|
||||
@@ -289,17 +289,17 @@ export function FileManagerSidebar({
|
||||
toggleFolder(item.id, item.path);
|
||||
onPathChange(item.path);
|
||||
} else if (item.type === "recent" || item.type === "pinned") {
|
||||
// 对于文件类型,调用文件打开回调
|
||||
// For file types, call file open callback
|
||||
if (onFileOpen) {
|
||||
onFileOpen(item);
|
||||
} else {
|
||||
// 如果没有文件打开回调,切换到文件所在目录
|
||||
// If no file open callback, switch to file directory
|
||||
const directory =
|
||||
item.path.substring(0, item.path.lastIndexOf("/")) || "/";
|
||||
onPathChange(directory);
|
||||
}
|
||||
} else if (item.type === "shortcut") {
|
||||
// 文件夹快捷方式直接切换到目录
|
||||
// Folder shortcuts directly switch to directory
|
||||
onPathChange(item.path);
|
||||
}
|
||||
};
|
||||
@@ -312,12 +312,12 @@ export function FileManagerSidebar({
|
||||
} else {
|
||||
newExpanded.add(folderId);
|
||||
|
||||
// 按需加载子目录
|
||||
// Load subdirectories on demand
|
||||
if (sshSessionId && folderPath && folderPath !== "/") {
|
||||
try {
|
||||
const subResponse = await listSSHFiles(sshSessionId, folderPath);
|
||||
|
||||
// listSSHFiles 现在总是返回 {files: Array, path: string} 格式
|
||||
// listSSHFiles now always returns {files: Array, path: string} format
|
||||
const subFiles = subResponse.files || [];
|
||||
const subFolders = subFiles.filter(
|
||||
(item: any) => item.type === "directory",
|
||||
@@ -332,7 +332,7 @@ export function FileManagerSidebar({
|
||||
children: [],
|
||||
}));
|
||||
|
||||
// 更新目录树,为当前文件夹添加子目录
|
||||
// Update directory tree, add subdirectories for current folder
|
||||
setDirectoryTree((prevTree) => {
|
||||
const updateChildren = (items: SidebarItem[]): SidebarItem[] => {
|
||||
return items.map((item) => {
|
||||
@@ -370,7 +370,7 @@ export function FileManagerSidebar({
|
||||
style={{ paddingLeft: `${12 + level * 16}px`, paddingRight: "12px" }}
|
||||
onClick={() => handleItemClick(item)}
|
||||
onContextMenu={(e) => {
|
||||
// 只有快捷功能项才需要右键菜单
|
||||
// Only quick access items need right-click menu
|
||||
if (
|
||||
item.type === "recent" ||
|
||||
item.type === "pinned" ||
|
||||
@@ -447,7 +447,7 @@ export function FileManagerSidebar({
|
||||
<div className="h-full flex flex-col bg-dark-bg border-r border-dark-border">
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
<div className="absolute inset-1.5 overflow-y-auto thin-scrollbar space-y-4">
|
||||
{/* 快捷功能区域 */}
|
||||
{/* Quick access area */}
|
||||
{renderSection(
|
||||
t("fileManager.recent"),
|
||||
<Clock className="w-3 h-3" />,
|
||||
@@ -464,7 +464,7 @@ export function FileManagerSidebar({
|
||||
shortcuts,
|
||||
)}
|
||||
|
||||
{/* 目录树 */}
|
||||
{/* Directory tree */}
|
||||
<div
|
||||
className={cn(
|
||||
hasQuickAccessItems && "pt-4 border-t border-dark-border",
|
||||
@@ -482,7 +482,7 @@ export function FileManagerSidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 */}
|
||||
{/* Right-click menu */}
|
||||
{contextMenu.isVisible && contextMenu.item && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" />
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { DiffEditor } from "@monaco-editor/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Download,
|
||||
RefreshCw,
|
||||
@@ -35,6 +36,7 @@ export function DiffViewer({
|
||||
onDownload1,
|
||||
onDownload2,
|
||||
}: DiffViewerProps) {
|
||||
const { t } = useTranslation();
|
||||
const [content1, setContent1] = useState<string>("");
|
||||
const [content2, setContent2] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -44,7 +46,7 @@ export function DiffViewer({
|
||||
);
|
||||
const [showLineNumbers, setShowLineNumbers] = useState(true);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
const ensureSSHConnection = async () => {
|
||||
try {
|
||||
const status = await getSSHStatus(sshSessionId);
|
||||
@@ -68,10 +70,10 @@ export function DiffViewer({
|
||||
}
|
||||
};
|
||||
|
||||
// 加载文件内容
|
||||
// Load file contents
|
||||
const loadFileContents = async () => {
|
||||
if (file1.type !== "file" || file2.type !== "file") {
|
||||
setError("只能对比文件类型的项目");
|
||||
setError(t("fileManager.canOnlyCompareFiles"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,10 +81,10 @@ export function DiffViewer({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
// 并行加载两个文件
|
||||
// Load both files in parallel
|
||||
const [response1, response2] = await Promise.all([
|
||||
readSSHFile(sshSessionId, file1.path),
|
||||
readSSHFile(sshSessionId, file2.path),
|
||||
@@ -95,17 +97,23 @@ export function DiffViewer({
|
||||
|
||||
const errorData = error?.response?.data;
|
||||
if (errorData?.tooLarge) {
|
||||
setError(`文件过大: ${errorData.error}`);
|
||||
setError(t("fileManager.fileTooLarge", { error: errorData.error }));
|
||||
} else if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
setError(
|
||||
`SSH连接失败。请检查与 ${sshHost.name} (${sshHost.ip}:${sshHost.port}) 的连接`,
|
||||
t("fileManager.sshConnectionFailed", {
|
||||
name: sshHost.name,
|
||||
ip: sshHost.ip,
|
||||
port: sshHost.port
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setError(
|
||||
`加载文件失败: ${error.message || errorData?.error || "未知错误"}`,
|
||||
t("fileManager.loadFileFailed", {
|
||||
error: error.message || errorData?.error || t("fileManager.unknownError")
|
||||
}),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -113,7 +121,7 @@ export function DiffViewer({
|
||||
}
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
// Download file
|
||||
const handleDownloadFile = async (file: FileItem) => {
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
@@ -147,7 +155,7 @@ export function DiffViewer({
|
||||
}
|
||||
};
|
||||
|
||||
// 获取文件语言类型
|
||||
// Get file language type
|
||||
const getFileLanguage = (fileName: string): string => {
|
||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||
const languageMap: Record<string, string> = {
|
||||
@@ -182,7 +190,7 @@ export function DiffViewer({
|
||||
return languageMap[ext || ""] || "plaintext";
|
||||
};
|
||||
|
||||
// 初始加载
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadFileContents();
|
||||
}, [file1, file2, sshSessionId]);
|
||||
@@ -192,7 +200,7 @@ export function DiffViewer({
|
||||
<div className="h-full flex items-center justify-center bg-dark-bg">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-sm text-muted-foreground">正在加载文件对比...</p>
|
||||
<p className="text-sm text-muted-foreground">{t("fileManager.loadingFileComparison")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -206,7 +214,7 @@ export function DiffViewer({
|
||||
<p className="text-red-500 mb-4">{error}</p>
|
||||
<Button onClick={loadFileContents} variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新加载
|
||||
{t("fileManager.reload")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,12 +223,12 @@ export function DiffViewer({
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-dark-bg">
|
||||
{/* 工具栏 */}
|
||||
{/* Toolbar */}
|
||||
<div className="flex-shrink-0 border-b border-dark-border p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">对比:</span>
|
||||
<span className="text-muted-foreground">{t("fileManager.compare")}:</span>
|
||||
<span className="font-medium text-green-400 mx-2">
|
||||
{file1.name}
|
||||
</span>
|
||||
@@ -230,7 +238,7 @@ export function DiffViewer({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 视图切换 */}
|
||||
{/* View toggle */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -240,10 +248,10 @@ export function DiffViewer({
|
||||
)
|
||||
}
|
||||
>
|
||||
{diffMode === "side-by-side" ? "并排" : "内联"}
|
||||
{diffMode === "side-by-side" ? t("fileManager.sideBySide") : t("fileManager.inline")}
|
||||
</Button>
|
||||
|
||||
{/* 行号切换 */}
|
||||
{/* Line number toggle */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -256,12 +264,12 @@ export function DiffViewer({
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
{/* Download buttons */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDownloadFile(file1)}
|
||||
title={`下载 ${file1.name}`}
|
||||
title={t("fileManager.downloadFile", { name: file1.name })}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
{file1.name}
|
||||
@@ -271,13 +279,13 @@ export function DiffViewer({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDownloadFile(file2)}
|
||||
title={`下载 ${file2.name}`}
|
||||
title={t("fileManager.downloadFile", { name: file2.name })}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
{file2.name}
|
||||
</Button>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
{/* Refresh button */}
|
||||
<Button variant="outline" size="sm" onClick={loadFileContents}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -285,7 +293,7 @@ export function DiffViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff编辑器 */}
|
||||
{/* Diff editor */}
|
||||
<div className="flex-1">
|
||||
<DiffEditor
|
||||
original={content1}
|
||||
@@ -314,7 +322,7 @@ export function DiffViewer({
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-sm text-muted-foreground">初始化编辑器...</p>
|
||||
<p className="text-sm text-muted-foreground">{t("fileManager.initializingEditor")}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Minus, Square, X, Maximize2, Minimize2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DraggableWindowProps {
|
||||
title: string;
|
||||
@@ -35,7 +36,8 @@ export function DraggableWindow({
|
||||
zIndex = 1000,
|
||||
onFocus,
|
||||
}: DraggableWindowProps) {
|
||||
// 窗口状态
|
||||
const { t } = useTranslation();
|
||||
// Window state
|
||||
const [position, setPosition] = useState({ x: initialX, y: initialY });
|
||||
const [size, setSize] = useState({
|
||||
width: initialWidth,
|
||||
@@ -45,19 +47,19 @@ export function DraggableWindow({
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [resizeDirection, setResizeDirection] = useState<string>("");
|
||||
|
||||
// 拖拽开始位置
|
||||
// Drag start position
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||
const [windowStart, setWindowStart] = useState({ x: 0, y: 0 });
|
||||
|
||||
const windowRef = useRef<HTMLDivElement>(null);
|
||||
const titleBarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 处理窗口焦点
|
||||
// Handle window focus
|
||||
const handleWindowClick = useCallback(() => {
|
||||
onFocus?.();
|
||||
}, [onFocus]);
|
||||
|
||||
// 拖拽处理
|
||||
// Drag handling
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (isMaximized) return;
|
||||
@@ -85,7 +87,7 @@ export function DraggableWindow({
|
||||
y: Math.max(
|
||||
0,
|
||||
Math.min(window.innerHeight - 40, windowStart.y + deltaY),
|
||||
), // 保持标题栏可见
|
||||
), // Keep title bar visible
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +145,7 @@ export function DraggableWindow({
|
||||
setResizeDirection("");
|
||||
}, []);
|
||||
|
||||
// 调整大小处理
|
||||
// Resize handling
|
||||
const handleResizeStart = useCallback(
|
||||
(e: React.MouseEvent, direction: string) => {
|
||||
if (isMaximized) return;
|
||||
@@ -159,7 +161,7 @@ export function DraggableWindow({
|
||||
[isMaximized, size, onFocus],
|
||||
);
|
||||
|
||||
// 全局事件监听
|
||||
// Global event listeners
|
||||
useEffect(() => {
|
||||
if (isDragging || isResizing) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
@@ -176,7 +178,7 @@ export function DraggableWindow({
|
||||
}
|
||||
}, [isDragging, isResizing, handleMouseMove, handleMouseUp]);
|
||||
|
||||
// 双击标题栏最大化/还原
|
||||
// Double-click title bar to maximize/restore
|
||||
const handleTitleDoubleClick = useCallback(() => {
|
||||
onMaximize?.();
|
||||
}, [onMaximize]);
|
||||
@@ -198,7 +200,7 @@ export function DraggableWindow({
|
||||
}}
|
||||
onClick={handleWindowClick}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
{/* Title bar */}
|
||||
<div
|
||||
ref={titleBarRef}
|
||||
className={cn(
|
||||
@@ -234,7 +236,7 @@ export function DraggableWindow({
|
||||
e.stopPropagation();
|
||||
onMaximize();
|
||||
}}
|
||||
title={isMaximized ? "还原" : "最大化"}
|
||||
title={isMaximized ? t("common.restore") : t("common.maximize")}
|
||||
>
|
||||
{isMaximized ? (
|
||||
<Minimize2 className="w-4 h-4" />
|
||||
@@ -257,7 +259,7 @@ export function DraggableWindow({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 窗口内容 */}
|
||||
{/* Window content */}
|
||||
<div
|
||||
className="flex-1 overflow-auto"
|
||||
style={{ height: "calc(100% - 40px)" }}
|
||||
@@ -265,10 +267,10 @@ export function DraggableWindow({
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* 调整大小边框 - 只在非最大化时显示 */}
|
||||
{/* Resize borders - only show when not maximized */}
|
||||
{!isMaximized && (
|
||||
<>
|
||||
{/* 边缘调整 */}
|
||||
{/* Edge resize */}
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 h-1 cursor-n-resize"
|
||||
onMouseDown={(e) => handleResizeStart(e, "top")}
|
||||
@@ -286,7 +288,7 @@ export function DraggableWindow({
|
||||
onMouseDown={(e) => handleResizeStart(e, "right")}
|
||||
/>
|
||||
|
||||
{/* 角落调整 */}
|
||||
{/* Corner resize */}
|
||||
<div
|
||||
className="absolute top-0 left-0 w-2 h-2 cursor-nw-resize"
|
||||
onMouseDown={(e) => handleResizeStart(e, "top-left")}
|
||||
|
||||
@@ -73,12 +73,12 @@ interface FileViewerProps {
|
||||
onDownload?: () => void;
|
||||
}
|
||||
|
||||
// 获取编程语言的官方图标
|
||||
// Get official icon for programming languages
|
||||
function getLanguageIcon(filename: string): React.ReactNode {
|
||||
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
||||
const baseName = filename.toLowerCase();
|
||||
|
||||
// 特殊文件名处理
|
||||
// Special filename handling
|
||||
if (["dockerfile"].includes(baseName)) {
|
||||
return <SiDocker className="w-6 h-6 text-blue-400" />;
|
||||
}
|
||||
@@ -124,7 +124,7 @@ function getLanguageIcon(filename: string): React.ReactNode {
|
||||
return iconMap[ext] || <Code className="w-6 h-6 text-yellow-500" />;
|
||||
}
|
||||
|
||||
// 获取文件类型和图标
|
||||
// Get file type and icon
|
||||
function getFileType(filename: string): {
|
||||
type: string;
|
||||
icon: React.ReactNode;
|
||||
@@ -209,17 +209,17 @@ function getFileType(filename: string): {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取CodeMirror语言扩展
|
||||
// Get CodeMirror language extension
|
||||
function getLanguageExtension(filename: string) {
|
||||
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
||||
const baseName = filename.toLowerCase();
|
||||
|
||||
// 特殊文件名处理
|
||||
// Special filename handling
|
||||
if (["dockerfile", "makefile", "rakefile", "gemfile"].includes(baseName)) {
|
||||
return loadLanguage(baseName);
|
||||
}
|
||||
|
||||
// 根据扩展名映射
|
||||
// Map by file extension
|
||||
const langMap: Record<string, string> = {
|
||||
js: "javascript",
|
||||
jsx: "jsx",
|
||||
@@ -258,7 +258,7 @@ function getLanguageExtension(filename: string) {
|
||||
return language ? loadLanguage(language) : null;
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
// Format file size
|
||||
function formatFileSize(bytes?: number): string {
|
||||
if (!bytes) return "Unknown size";
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
@@ -294,31 +294,31 @@ export function FileViewer({
|
||||
|
||||
const fileTypeInfo = getFileType(file.name);
|
||||
|
||||
// 文件大小限制 - 移除硬限制,支持大文件处理
|
||||
const WARNING_SIZE = 50 * 1024 * 1024; // 50MB 警告
|
||||
const MAX_SIZE = Number.MAX_SAFE_INTEGER; // 移除硬限制
|
||||
// File size limits - remove hard limits, support large file handling
|
||||
const WARNING_SIZE = 50 * 1024 * 1024; // 50MB warning
|
||||
const MAX_SIZE = Number.MAX_SAFE_INTEGER; // Remove hard limits
|
||||
|
||||
// 检查是否应该显示为文本
|
||||
// Check if should display as text
|
||||
const shouldShowAsText =
|
||||
fileTypeInfo.type === "text" ||
|
||||
fileTypeInfo.type === "code" ||
|
||||
(fileTypeInfo.type === "unknown" &&
|
||||
(forceShowAsText || !file.size || file.size <= WARNING_SIZE));
|
||||
|
||||
// 检查文件是否过大
|
||||
// Check if file is too large
|
||||
const isLargeFile = file.size && file.size > WARNING_SIZE;
|
||||
const isTooLarge = file.size && file.size > MAX_SIZE;
|
||||
|
||||
// 同步外部内容更改
|
||||
// Sync external content changes
|
||||
useEffect(() => {
|
||||
setEditedContent(content);
|
||||
// 只有在savedContent更新时才更新originalContent
|
||||
// Only update originalContent when savedContent is updated
|
||||
if (savedContent) {
|
||||
setOriginalContent(savedContent);
|
||||
}
|
||||
setHasChanges(content !== (savedContent || content));
|
||||
|
||||
// 如果是未知文件类型且文件较大,显示警告
|
||||
// If unknown file type and file is large, show warning
|
||||
if (fileTypeInfo.type === "unknown" && isLargeFile && !forceShowAsText) {
|
||||
setShowLargeFileWarning(true);
|
||||
} else {
|
||||
@@ -326,27 +326,27 @@ export function FileViewer({
|
||||
}
|
||||
}, [content, savedContent, fileTypeInfo.type, isLargeFile, forceShowAsText]);
|
||||
|
||||
// 处理内容更改
|
||||
// Handle content changes
|
||||
const handleContentChange = (newContent: string) => {
|
||||
setEditedContent(newContent);
|
||||
setHasChanges(newContent !== originalContent);
|
||||
onContentChange?.(newContent);
|
||||
};
|
||||
|
||||
// 保存文件
|
||||
// Save file
|
||||
const handleSave = () => {
|
||||
onSave?.(editedContent);
|
||||
// 注意:不在这里更新originalContent,因为它会通过savedContent prop更新
|
||||
// Note: Don't update originalContent here, as it will be updated via savedContent prop
|
||||
};
|
||||
|
||||
// 复原文件
|
||||
// Revert file
|
||||
const handleRevert = () => {
|
||||
setEditedContent(originalContent);
|
||||
setHasChanges(false);
|
||||
onContentChange?.(originalContent);
|
||||
};
|
||||
|
||||
// 搜索匹配功能
|
||||
// Search matching functionality
|
||||
const findMatches = (text: string) => {
|
||||
if (!text) {
|
||||
setSearchMatches([]);
|
||||
@@ -363,7 +363,7 @@ export function FileViewer({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
});
|
||||
// 避免无限循环
|
||||
// Avoid infinite loop
|
||||
if (match.index === regex.lastIndex) regex.lastIndex++;
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ export function FileViewer({
|
||||
setCurrentMatchIndex(matches.length > 0 ? 0 : -1);
|
||||
};
|
||||
|
||||
// 搜索导航
|
||||
// Search navigation
|
||||
const goToNextMatch = () => {
|
||||
if (searchMatches.length === 0) return;
|
||||
setCurrentMatchIndex((prev) => (prev + 1) % searchMatches.length);
|
||||
@@ -384,7 +384,7 @@ export function FileViewer({
|
||||
);
|
||||
};
|
||||
|
||||
// 替换功能
|
||||
// Replace functionality
|
||||
const handleFindReplace = (
|
||||
findText: string,
|
||||
replaceWithText: string,
|
||||
@@ -399,7 +399,7 @@ export function FileViewer({
|
||||
replaceWithText,
|
||||
);
|
||||
} else if (currentMatchIndex >= 0 && searchMatches[currentMatchIndex]) {
|
||||
// 替换当前匹配项
|
||||
// Replace current match
|
||||
const match = searchMatches[currentMatchIndex];
|
||||
newContent =
|
||||
editedContent.substring(0, match.start) +
|
||||
@@ -411,7 +411,7 @@ export function FileViewer({
|
||||
setHasChanges(newContent !== originalContent);
|
||||
onContentChange?.(newContent);
|
||||
|
||||
// 重新搜索以更新匹配项
|
||||
// Re-search to update matches
|
||||
setTimeout(() => findMatches(findText), 0);
|
||||
};
|
||||
|
||||
@@ -425,7 +425,7 @@ export function FileViewer({
|
||||
setShowReplacePanel(true);
|
||||
};
|
||||
|
||||
// 渲染带高亮的文本
|
||||
// Render highlighted text
|
||||
const renderHighlightedText = (text: string) => {
|
||||
if (!searchText || searchMatches.length === 0) {
|
||||
return text;
|
||||
@@ -435,12 +435,12 @@ export function FileViewer({
|
||||
let lastIndex = 0;
|
||||
|
||||
searchMatches.forEach((match, index) => {
|
||||
// 添加匹配前的文本
|
||||
// Add text before match
|
||||
if (match.start > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, match.start));
|
||||
}
|
||||
|
||||
// 添加高亮的匹配文本
|
||||
// Add highlighted match text
|
||||
const isCurrentMatch = index === currentMatchIndex;
|
||||
parts.push(
|
||||
<span
|
||||
@@ -459,7 +459,7 @@ export function FileViewer({
|
||||
lastIndex = match.end;
|
||||
});
|
||||
|
||||
// 添加最后的文本
|
||||
// Add final text
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex));
|
||||
}
|
||||
@@ -467,13 +467,13 @@ export function FileViewer({
|
||||
return parts;
|
||||
};
|
||||
|
||||
// 处理用户确认打开大文件
|
||||
// Handle user confirmation to open large file
|
||||
const handleConfirmOpenAsText = () => {
|
||||
setForceShowAsText(true);
|
||||
setShowLargeFileWarning(false);
|
||||
};
|
||||
|
||||
// 处理用户拒绝打开大文件
|
||||
// Handle user rejection to open large file
|
||||
const handleCancelOpenAsText = () => {
|
||||
setShowLargeFileWarning(false);
|
||||
};
|
||||
@@ -491,7 +491,7 @@ export function FileViewer({
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
{/* 文件信息头部 */}
|
||||
{/* File info header */}
|
||||
<div className="flex-shrink-0 bg-card border-b border-border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -517,7 +517,7 @@ export function FileViewer({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 编辑工具栏 - 直接显示,无需切换 */}
|
||||
{/* Edit toolbar - display directly, no toggle needed */}
|
||||
{isEditable && (
|
||||
<>
|
||||
<Button
|
||||
@@ -575,7 +575,7 @@ export function FileViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索和替换面板 */}
|
||||
{/* Search and replace panel */}
|
||||
{showSearchPanel && (
|
||||
<div className="flex-shrink-0 bg-muted/30 border-b border-border p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
@@ -657,9 +657,9 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文件内容 */}
|
||||
{/* File content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 大文件警告对话框 */}
|
||||
{/* Large file warning dialog */}
|
||||
{showLargeFileWarning && (
|
||||
<div className="h-full flex items-center justify-center bg-background">
|
||||
<div className="bg-card border border-destructive/30 rounded-lg p-6 max-w-md mx-4 shadow-lg">
|
||||
@@ -722,7 +722,7 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图片预览 */}
|
||||
{/* Image preview */}
|
||||
{fileTypeInfo.type === "image" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
<img
|
||||
@@ -737,16 +737,16 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文本和代码文件预览 */}
|
||||
{/* Text and code file preview */}
|
||||
{shouldShowAsText && !showLargeFileWarning && (
|
||||
<div className="h-full flex flex-col">
|
||||
{fileTypeInfo.type === "code" ? (
|
||||
// 代码文件使用CodeMirror
|
||||
// Code files use CodeMirror
|
||||
<div className="h-full">
|
||||
{searchText && searchMatches.length > 0 ? (
|
||||
// 当有搜索结果时,显示只读的高亮文本(带行号)
|
||||
// When there are search results, show read-only highlighted text (with line numbers)
|
||||
<div className="h-full flex bg-muted">
|
||||
{/* 行号列 */}
|
||||
{/* Line number column */}
|
||||
<div className="flex-shrink-0 bg-muted border-r border-border px-2 py-4 text-xs text-muted-foreground font-mono select-none">
|
||||
{editedContent.split("\n").map((_, index) => (
|
||||
<div
|
||||
@@ -757,13 +757,13 @@ export function FileViewer({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 代码内容 */}
|
||||
{/* Code content */}
|
||||
<div className="flex-1 p-4 font-mono text-sm whitespace-pre-wrap overflow-auto text-foreground">
|
||||
{renderHighlightedText(editedContent)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 没有搜索时显示CodeMirror编辑器
|
||||
// Show CodeMirror editor when no search
|
||||
<CodeMirror
|
||||
value={editedContent}
|
||||
onChange={(value) => handleContentChange(value)}
|
||||
@@ -790,17 +790,17 @@ export function FileViewer({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 普通文本文件
|
||||
// Plain text files
|
||||
<div className="h-full">
|
||||
{isEditable ? (
|
||||
<div className="h-full">
|
||||
{searchText && searchMatches.length > 0 ? (
|
||||
// 当有搜索结果时,显示只读的高亮文本
|
||||
// When there are search results, show read-only highlighted text
|
||||
<div className="h-full p-4 font-mono text-sm whitespace-pre-wrap overflow-auto bg-background text-foreground">
|
||||
{renderHighlightedText(editedContent)}
|
||||
</div>
|
||||
) : (
|
||||
// 直接显示可编辑的textarea
|
||||
// Directly show editable textarea
|
||||
<textarea
|
||||
value={editedContent}
|
||||
onChange={(e) => handleContentChange(e.target.value)}
|
||||
@@ -811,7 +811,7 @@ export function FileViewer({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 只有非可编辑文件(媒体文件)才显示为只读
|
||||
// Only show as read-only for non-editable files (media files)
|
||||
<div className="h-full p-4 font-mono text-sm whitespace-pre-wrap overflow-auto bg-background text-foreground">
|
||||
{editedContent || content || "File is empty"}
|
||||
</div>
|
||||
@@ -821,7 +821,7 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 视频文件预览 */}
|
||||
{/* Video file preview */}
|
||||
{fileTypeInfo.type === "video" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
<video
|
||||
@@ -834,7 +834,7 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 音频文件预览 */}
|
||||
{/* Audio file preview */}
|
||||
{fileTypeInfo.type === "audio" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
@@ -857,7 +857,7 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 未知文件类型 - 只在不能显示为文本且没有警告时显示 */}
|
||||
{/* Unknown file type - only show when cannot display as text and no warning */}
|
||||
{fileTypeInfo.type === "unknown" &&
|
||||
!shouldShowAsText &&
|
||||
!showLargeFileWarning && (
|
||||
@@ -886,7 +886,7 @@ export function FileViewer({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部状态栏 */}
|
||||
{/* Bottom status bar */}
|
||||
<div className="flex-shrink-0 bg-muted/50 border-t border-border px-4 py-2 text-xs text-muted-foreground">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>{file.path}</span>
|
||||
|
||||
@@ -43,7 +43,7 @@ interface FileWindowProps {
|
||||
sshHost: SSHHost;
|
||||
initialX?: number;
|
||||
initialY?: number;
|
||||
// readOnly参数已移除,由FileViewer内部根据文件类型决定
|
||||
// readOnly parameter removed, determined internally by FileViewer based on file type
|
||||
}
|
||||
|
||||
export function FileWindow({
|
||||
@@ -71,17 +71,17 @@ export function FileWindow({
|
||||
|
||||
const currentWindow = windows.find((w) => w.id === windowId);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
const ensureSSHConnection = async () => {
|
||||
try {
|
||||
// 首先检查SSH连接状态
|
||||
// First check SSH connection status
|
||||
const status = await getSSHStatus(sshSessionId);
|
||||
console.log("SSH connection status:", status);
|
||||
|
||||
if (!status.connected) {
|
||||
console.log("SSH not connected, attempting to reconnect...");
|
||||
|
||||
// 重新建立连接
|
||||
// Re-establish connection
|
||||
await connectSSH(sshSessionId, {
|
||||
hostId: sshHost.id,
|
||||
ip: sshHost.ip,
|
||||
@@ -99,12 +99,12 @@ export function FileWindow({
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("SSH connection check/reconnect failed:", error);
|
||||
// 即使连接失败也尝试继续,让具体的API调用报错
|
||||
// Even if connection fails, try to continue and let specific API calls handle errors
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载文件内容
|
||||
// Load file content
|
||||
useEffect(() => {
|
||||
const loadFileContent = async () => {
|
||||
if (file.type !== "file") return;
|
||||
@@ -112,23 +112,23 @@ export function FileWindow({
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
const response = await readSSHFile(sshSessionId, file.path);
|
||||
const fileContent = response.content || "";
|
||||
setContent(fileContent);
|
||||
setPendingContent(fileContent); // 初始化待保存内容
|
||||
setPendingContent(fileContent); // Initialize pending content
|
||||
|
||||
// 如果文件大小未知,根据内容计算大小
|
||||
// If file size is unknown, calculate size based on content
|
||||
if (!file.size) {
|
||||
const contentSize = new Blob([fileContent]).size;
|
||||
file.size = contentSize;
|
||||
}
|
||||
|
||||
// 根据文件类型决定是否可编辑:除了媒体文件,其他都可编辑
|
||||
// Determine if editable based on file type: all except media files are editable
|
||||
const mediaExtensions = [
|
||||
// 图片文件
|
||||
// Image files
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
@@ -138,7 +138,7 @@ export function FileWindow({
|
||||
"webp",
|
||||
"tiff",
|
||||
"ico",
|
||||
// 音频文件
|
||||
// Audio files
|
||||
"mp3",
|
||||
"wav",
|
||||
"ogg",
|
||||
@@ -146,7 +146,7 @@ export function FileWindow({
|
||||
"flac",
|
||||
"m4a",
|
||||
"wma",
|
||||
// 视频文件
|
||||
// Video files
|
||||
"mp4",
|
||||
"avi",
|
||||
"mov",
|
||||
@@ -155,7 +155,7 @@ export function FileWindow({
|
||||
"mkv",
|
||||
"webm",
|
||||
"m4v",
|
||||
// 压缩文件
|
||||
// Archive files
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
@@ -163,7 +163,7 @@ export function FileWindow({
|
||||
"gz",
|
||||
"bz2",
|
||||
"xz",
|
||||
// 二进制文件
|
||||
// Binary files
|
||||
"exe",
|
||||
"dll",
|
||||
"so",
|
||||
@@ -173,12 +173,12 @@ export function FileWindow({
|
||||
];
|
||||
|
||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||
// 只有媒体文件和二进制文件不可编辑,其他所有文件都可编辑
|
||||
// Only media files and binary files are not editable, all other files are editable
|
||||
setIsEditable(!mediaExtensions.includes(extension || ""));
|
||||
} catch (error: any) {
|
||||
console.error("Failed to load file:", error);
|
||||
|
||||
// 检查是否是大文件错误
|
||||
// Check if it's a large file error
|
||||
const errorData = error?.response?.data;
|
||||
if (errorData?.tooLarge) {
|
||||
toast.error(`File too large: ${errorData.error}`, {
|
||||
@@ -188,7 +188,7 @@ export function FileWindow({
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
// 如果是连接错误,提供更明确的错误信息
|
||||
// If connection error, provide more specific error message
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||
);
|
||||
@@ -205,19 +205,19 @@ export function FileWindow({
|
||||
loadFileContent();
|
||||
}, [file, sshSessionId, sshHost]);
|
||||
|
||||
// 保存文件
|
||||
// Save file
|
||||
const handleSave = async (newContent: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
await writeSSHFile(sshSessionId, file.path, newContent);
|
||||
setContent(newContent);
|
||||
setPendingContent(""); // 清除待保存内容
|
||||
setPendingContent(""); // Clear pending content
|
||||
|
||||
// 清除自动保存定时器
|
||||
// Clear auto-save timer
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current);
|
||||
autoSaveTimerRef.current = null;
|
||||
@@ -227,7 +227,7 @@ export function FileWindow({
|
||||
} catch (error: any) {
|
||||
console.error("Failed to save file:", error);
|
||||
|
||||
// 如果是连接错误,提供更明确的错误信息
|
||||
// If it's a connection error, provide more specific error message
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
@@ -243,16 +243,16 @@ export function FileWindow({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理内容变更 - 设置1分钟自动保存
|
||||
// Handle content changes - set 1-minute auto-save
|
||||
const handleContentChange = (newContent: string) => {
|
||||
setPendingContent(newContent);
|
||||
|
||||
// 清除之前的定时器
|
||||
// Clear previous timer
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current);
|
||||
}
|
||||
|
||||
// 设置新的1分钟自动保存定时器
|
||||
// Set new 1-minute auto-save timer
|
||||
autoSaveTimerRef.current = setTimeout(async () => {
|
||||
try {
|
||||
console.log("Auto-saving file...");
|
||||
@@ -262,10 +262,10 @@ export function FileWindow({
|
||||
console.error("Auto-save failed:", error);
|
||||
toast.error(t("fileManager.autoSaveFailed"));
|
||||
}
|
||||
}, 60000); // 1分钟 = 60000毫秒
|
||||
}, 60000); // 1 minute = 60000 milliseconds
|
||||
};
|
||||
|
||||
// 清理定时器
|
||||
// Cleanup timer
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoSaveTimerRef.current) {
|
||||
@@ -274,10 +274,10 @@ export function FileWindow({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 下载文件
|
||||
// Download file
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
const response = await downloadSSHFile(sshSessionId, file.path);
|
||||
@@ -308,7 +308,7 @@ export function FileWindow({
|
||||
} catch (error: any) {
|
||||
console.error("Failed to download file:", error);
|
||||
|
||||
// 如果是连接错误,提供更明确的错误信息
|
||||
// If it's a connection error, provide more specific error message
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
@@ -324,7 +324,7 @@ export function FileWindow({
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口操作处理
|
||||
// Window operation handling
|
||||
const handleClose = () => {
|
||||
closeWindow(windowId);
|
||||
};
|
||||
@@ -366,7 +366,7 @@ export function FileWindow({
|
||||
content={pendingContent || content}
|
||||
savedContent={content}
|
||||
isLoading={isLoading}
|
||||
isEditable={isEditable} // 移除强制只读模式,由FileViewer内部控制
|
||||
isEditable={isEditable} // Remove forced read-only mode, controlled internally by FileViewer
|
||||
onContentChange={handleContentChange}
|
||||
onSave={(newContent) => handleSave(newContent)}
|
||||
onDownload={handleDownload}
|
||||
|
||||
@@ -35,13 +35,13 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
const nextZIndex = useRef(1000);
|
||||
const windowCounter = useRef(0);
|
||||
|
||||
// 打开新窗口
|
||||
// Open new window
|
||||
const openWindow = useCallback(
|
||||
(windowData: Omit<WindowInstance, "id" | "zIndex">) => {
|
||||
const id = `window-${++windowCounter.current}`;
|
||||
const zIndex = ++nextZIndex.current;
|
||||
|
||||
// 计算偏移位置,避免窗口完全重叠
|
||||
// Calculate offset position to avoid windows completely overlapping
|
||||
const offset = (windows.length % 5) * 30;
|
||||
const adjustedX = windowData.x + offset;
|
||||
const adjustedY = windowData.y + offset;
|
||||
@@ -60,12 +60,12 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
[windows.length],
|
||||
);
|
||||
|
||||
// 关闭窗口
|
||||
// Close window
|
||||
const closeWindow = useCallback((id: string) => {
|
||||
setWindows((prev) => prev.filter((w) => w.id !== id));
|
||||
}, []);
|
||||
|
||||
// 最小化窗口
|
||||
// Minimize window
|
||||
const minimizeWindow = useCallback((id: string) => {
|
||||
setWindows((prev) =>
|
||||
prev.map((w) =>
|
||||
@@ -74,7 +74,7 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 最大化/还原窗口
|
||||
// Maximize/restore window
|
||||
const maximizeWindow = useCallback((id: string) => {
|
||||
setWindows((prev) =>
|
||||
prev.map((w) =>
|
||||
@@ -83,7 +83,7 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 聚焦窗口 (置于顶层)
|
||||
// Focus window (bring to top)
|
||||
const focusWindow = useCallback((id: string) => {
|
||||
setWindows((prev) => {
|
||||
const targetWindow = prev.find((w) => w.id === id);
|
||||
@@ -94,7 +94,7 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 更新窗口属性
|
||||
// Update window properties
|
||||
const updateWindow = useCallback(
|
||||
(id: string, updates: Partial<WindowInstance>) => {
|
||||
setWindows((prev) =>
|
||||
@@ -117,7 +117,7 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
return (
|
||||
<WindowManagerContext.Provider value={contextValue}>
|
||||
{children}
|
||||
{/* 渲染所有窗口 */}
|
||||
{/* Render all windows */}
|
||||
<div className="window-container">
|
||||
{windows.map((window) => (
|
||||
<div key={window.id}>
|
||||
|
||||
@@ -79,17 +79,17 @@ export function DragIndicator({
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* 图标 */}
|
||||
{/* Icon */}
|
||||
<div className="flex-shrink-0 mt-0.5">{getIcon()}</div>
|
||||
|
||||
{/* 内容 */}
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* 标题 */}
|
||||
{/* Title */}
|
||||
<div className="text-sm font-medium text-foreground mb-2">
|
||||
{fileCount > 1 ? "批量拖拽到桌面" : "拖拽到桌面"}
|
||||
</div>
|
||||
|
||||
{/* 状态文字 */}
|
||||
{/* Status text */}
|
||||
<div
|
||||
className={cn(
|
||||
"text-xs mb-3",
|
||||
@@ -103,7 +103,7 @@ export function DragIndicator({
|
||||
{getStatusText()}
|
||||
</div>
|
||||
|
||||
{/* 进度条 */}
|
||||
{/* Progress bar */}
|
||||
{(isDownloading || isDragging) && !error && (
|
||||
<div className="w-full bg-dark-border rounded-full h-2 mb-2">
|
||||
<div
|
||||
@@ -116,14 +116,14 @@ export function DragIndicator({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 进度百分比 */}
|
||||
{/* Progress percentage */}
|
||||
{(isDownloading || isDragging) && !error && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{progress.toFixed(0)}%
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 拖拽提示 */}
|
||||
{/* Drag hint */}
|
||||
{isDragging && !error && (
|
||||
<div className="text-xs text-green-500 mt-2 flex items-center gap-1">
|
||||
<Download className="w-3 h-3" />
|
||||
@@ -133,7 +133,7 @@ export function DragIndicator({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 动画效果的背景 */}
|
||||
{/* Background with animation effect */}
|
||||
{isDragging && !error && (
|
||||
<div className="absolute inset-0 rounded-lg bg-green-500/5 animate-pulse" />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user