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:
ZacharyZcR
2025-09-22 02:07:08 +08:00
parent 03e876dae9
commit d693dc5a14
10 changed files with 363 additions and 353 deletions

View File

@@ -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"

View File

@@ -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({
}
};
// 同步editPathValuecurrentPath
// 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,

View File

@@ -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}`;

View File

@@ -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" />

View File

@@ -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>
}

View File

@@ -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")}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}>

View File

@@ -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" />
)}