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

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