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:
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { DiffEditor } from "@monaco-editor/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Download,
|
||||
RefreshCw,
|
||||
@@ -35,6 +36,7 @@ export function DiffViewer({
|
||||
onDownload1,
|
||||
onDownload2,
|
||||
}: DiffViewerProps) {
|
||||
const { t } = useTranslation();
|
||||
const [content1, setContent1] = useState<string>("");
|
||||
const [content2, setContent2] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -44,7 +46,7 @@ export function DiffViewer({
|
||||
);
|
||||
const [showLineNumbers, setShowLineNumbers] = useState(true);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
const ensureSSHConnection = async () => {
|
||||
try {
|
||||
const status = await getSSHStatus(sshSessionId);
|
||||
@@ -68,10 +70,10 @@ export function DiffViewer({
|
||||
}
|
||||
};
|
||||
|
||||
// 加载文件内容
|
||||
// Load file contents
|
||||
const loadFileContents = async () => {
|
||||
if (file1.type !== "file" || file2.type !== "file") {
|
||||
setError("只能对比文件类型的项目");
|
||||
setError(t("fileManager.canOnlyCompareFiles"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -79,10 +81,10 @@ export function DiffViewer({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
// 并行加载两个文件
|
||||
// Load both files in parallel
|
||||
const [response1, response2] = await Promise.all([
|
||||
readSSHFile(sshSessionId, file1.path),
|
||||
readSSHFile(sshSessionId, file2.path),
|
||||
@@ -95,17 +97,23 @@ export function DiffViewer({
|
||||
|
||||
const errorData = error?.response?.data;
|
||||
if (errorData?.tooLarge) {
|
||||
setError(`文件过大: ${errorData.error}`);
|
||||
setError(t("fileManager.fileTooLarge", { error: errorData.error }));
|
||||
} else if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
setError(
|
||||
`SSH连接失败。请检查与 ${sshHost.name} (${sshHost.ip}:${sshHost.port}) 的连接`,
|
||||
t("fileManager.sshConnectionFailed", {
|
||||
name: sshHost.name,
|
||||
ip: sshHost.ip,
|
||||
port: sshHost.port
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setError(
|
||||
`加载文件失败: ${error.message || errorData?.error || "未知错误"}`,
|
||||
t("fileManager.loadFileFailed", {
|
||||
error: error.message || errorData?.error || t("fileManager.unknownError")
|
||||
}),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
@@ -113,7 +121,7 @@ export function DiffViewer({
|
||||
}
|
||||
};
|
||||
|
||||
// 下载文件
|
||||
// Download file
|
||||
const handleDownloadFile = async (file: FileItem) => {
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
@@ -147,7 +155,7 @@ export function DiffViewer({
|
||||
}
|
||||
};
|
||||
|
||||
// 获取文件语言类型
|
||||
// Get file language type
|
||||
const getFileLanguage = (fileName: string): string => {
|
||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||
const languageMap: Record<string, string> = {
|
||||
@@ -182,7 +190,7 @@ export function DiffViewer({
|
||||
return languageMap[ext || ""] || "plaintext";
|
||||
};
|
||||
|
||||
// 初始加载
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadFileContents();
|
||||
}, [file1, file2, sshSessionId]);
|
||||
@@ -192,7 +200,7 @@ export function DiffViewer({
|
||||
<div className="h-full flex items-center justify-center bg-dark-bg">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-sm text-muted-foreground">正在加载文件对比...</p>
|
||||
<p className="text-sm text-muted-foreground">{t("fileManager.loadingFileComparison")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -206,7 +214,7 @@ export function DiffViewer({
|
||||
<p className="text-red-500 mb-4">{error}</p>
|
||||
<Button onClick={loadFileContents} variant="outline">
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
重新加载
|
||||
{t("fileManager.reload")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -215,12 +223,12 @@ export function DiffViewer({
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-dark-bg">
|
||||
{/* 工具栏 */}
|
||||
{/* Toolbar */}
|
||||
<div className="flex-shrink-0 border-b border-dark-border p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">对比:</span>
|
||||
<span className="text-muted-foreground">{t("fileManager.compare")}:</span>
|
||||
<span className="font-medium text-green-400 mx-2">
|
||||
{file1.name}
|
||||
</span>
|
||||
@@ -230,7 +238,7 @@ export function DiffViewer({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 视图切换 */}
|
||||
{/* View toggle */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -240,10 +248,10 @@ export function DiffViewer({
|
||||
)
|
||||
}
|
||||
>
|
||||
{diffMode === "side-by-side" ? "并排" : "内联"}
|
||||
{diffMode === "side-by-side" ? t("fileManager.sideBySide") : t("fileManager.inline")}
|
||||
</Button>
|
||||
|
||||
{/* 行号切换 */}
|
||||
{/* Line number toggle */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -256,12 +264,12 @@ export function DiffViewer({
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* 下载按钮 */}
|
||||
{/* Download buttons */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDownloadFile(file1)}
|
||||
title={`下载 ${file1.name}`}
|
||||
title={t("fileManager.downloadFile", { name: file1.name })}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
{file1.name}
|
||||
@@ -271,13 +279,13 @@ export function DiffViewer({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDownloadFile(file2)}
|
||||
title={`下载 ${file2.name}`}
|
||||
title={t("fileManager.downloadFile", { name: file2.name })}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-1" />
|
||||
{file2.name}
|
||||
</Button>
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
{/* Refresh button */}
|
||||
<Button variant="outline" size="sm" onClick={loadFileContents}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -285,7 +293,7 @@ export function DiffViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff编辑器 */}
|
||||
{/* Diff editor */}
|
||||
<div className="flex-1">
|
||||
<DiffEditor
|
||||
original={content1}
|
||||
@@ -314,7 +322,7 @@ export function DiffViewer({
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-sm text-muted-foreground">初始化编辑器...</p>
|
||||
<p className="text-sm text-muted-foreground">{t("fileManager.initializingEditor")}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Minus, Square, X, Maximize2, Minimize2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DraggableWindowProps {
|
||||
title: string;
|
||||
@@ -35,7 +36,8 @@ export function DraggableWindow({
|
||||
zIndex = 1000,
|
||||
onFocus,
|
||||
}: DraggableWindowProps) {
|
||||
// 窗口状态
|
||||
const { t } = useTranslation();
|
||||
// Window state
|
||||
const [position, setPosition] = useState({ x: initialX, y: initialY });
|
||||
const [size, setSize] = useState({
|
||||
width: initialWidth,
|
||||
@@ -45,19 +47,19 @@ export function DraggableWindow({
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [resizeDirection, setResizeDirection] = useState<string>("");
|
||||
|
||||
// 拖拽开始位置
|
||||
// Drag start position
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||
const [windowStart, setWindowStart] = useState({ x: 0, y: 0 });
|
||||
|
||||
const windowRef = useRef<HTMLDivElement>(null);
|
||||
const titleBarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// 处理窗口焦点
|
||||
// Handle window focus
|
||||
const handleWindowClick = useCallback(() => {
|
||||
onFocus?.();
|
||||
}, [onFocus]);
|
||||
|
||||
// 拖拽处理
|
||||
// Drag handling
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (isMaximized) return;
|
||||
@@ -85,7 +87,7 @@ export function DraggableWindow({
|
||||
y: Math.max(
|
||||
0,
|
||||
Math.min(window.innerHeight - 40, windowStart.y + deltaY),
|
||||
), // 保持标题栏可见
|
||||
), // Keep title bar visible
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +145,7 @@ export function DraggableWindow({
|
||||
setResizeDirection("");
|
||||
}, []);
|
||||
|
||||
// 调整大小处理
|
||||
// Resize handling
|
||||
const handleResizeStart = useCallback(
|
||||
(e: React.MouseEvent, direction: string) => {
|
||||
if (isMaximized) return;
|
||||
@@ -159,7 +161,7 @@ export function DraggableWindow({
|
||||
[isMaximized, size, onFocus],
|
||||
);
|
||||
|
||||
// 全局事件监听
|
||||
// Global event listeners
|
||||
useEffect(() => {
|
||||
if (isDragging || isResizing) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
@@ -176,7 +178,7 @@ export function DraggableWindow({
|
||||
}
|
||||
}, [isDragging, isResizing, handleMouseMove, handleMouseUp]);
|
||||
|
||||
// 双击标题栏最大化/还原
|
||||
// Double-click title bar to maximize/restore
|
||||
const handleTitleDoubleClick = useCallback(() => {
|
||||
onMaximize?.();
|
||||
}, [onMaximize]);
|
||||
@@ -198,7 +200,7 @@ export function DraggableWindow({
|
||||
}}
|
||||
onClick={handleWindowClick}
|
||||
>
|
||||
{/* 标题栏 */}
|
||||
{/* Title bar */}
|
||||
<div
|
||||
ref={titleBarRef}
|
||||
className={cn(
|
||||
@@ -234,7 +236,7 @@ export function DraggableWindow({
|
||||
e.stopPropagation();
|
||||
onMaximize();
|
||||
}}
|
||||
title={isMaximized ? "还原" : "最大化"}
|
||||
title={isMaximized ? t("common.restore") : t("common.maximize")}
|
||||
>
|
||||
{isMaximized ? (
|
||||
<Minimize2 className="w-4 h-4" />
|
||||
@@ -257,7 +259,7 @@ export function DraggableWindow({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 窗口内容 */}
|
||||
{/* Window content */}
|
||||
<div
|
||||
className="flex-1 overflow-auto"
|
||||
style={{ height: "calc(100% - 40px)" }}
|
||||
@@ -265,10 +267,10 @@ export function DraggableWindow({
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* 调整大小边框 - 只在非最大化时显示 */}
|
||||
{/* Resize borders - only show when not maximized */}
|
||||
{!isMaximized && (
|
||||
<>
|
||||
{/* 边缘调整 */}
|
||||
{/* Edge resize */}
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 h-1 cursor-n-resize"
|
||||
onMouseDown={(e) => handleResizeStart(e, "top")}
|
||||
@@ -286,7 +288,7 @@ export function DraggableWindow({
|
||||
onMouseDown={(e) => handleResizeStart(e, "right")}
|
||||
/>
|
||||
|
||||
{/* 角落调整 */}
|
||||
{/* Corner resize */}
|
||||
<div
|
||||
className="absolute top-0 left-0 w-2 h-2 cursor-nw-resize"
|
||||
onMouseDown={(e) => handleResizeStart(e, "top-left")}
|
||||
|
||||
@@ -73,12 +73,12 @@ interface FileViewerProps {
|
||||
onDownload?: () => void;
|
||||
}
|
||||
|
||||
// 获取编程语言的官方图标
|
||||
// Get official icon for programming languages
|
||||
function getLanguageIcon(filename: string): React.ReactNode {
|
||||
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
||||
const baseName = filename.toLowerCase();
|
||||
|
||||
// 特殊文件名处理
|
||||
// Special filename handling
|
||||
if (["dockerfile"].includes(baseName)) {
|
||||
return <SiDocker className="w-6 h-6 text-blue-400" />;
|
||||
}
|
||||
@@ -124,7 +124,7 @@ function getLanguageIcon(filename: string): React.ReactNode {
|
||||
return iconMap[ext] || <Code className="w-6 h-6 text-yellow-500" />;
|
||||
}
|
||||
|
||||
// 获取文件类型和图标
|
||||
// Get file type and icon
|
||||
function getFileType(filename: string): {
|
||||
type: string;
|
||||
icon: React.ReactNode;
|
||||
@@ -209,17 +209,17 @@ function getFileType(filename: string): {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取CodeMirror语言扩展
|
||||
// Get CodeMirror language extension
|
||||
function getLanguageExtension(filename: string) {
|
||||
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
||||
const baseName = filename.toLowerCase();
|
||||
|
||||
// 特殊文件名处理
|
||||
// Special filename handling
|
||||
if (["dockerfile", "makefile", "rakefile", "gemfile"].includes(baseName)) {
|
||||
return loadLanguage(baseName);
|
||||
}
|
||||
|
||||
// 根据扩展名映射
|
||||
// Map by file extension
|
||||
const langMap: Record<string, string> = {
|
||||
js: "javascript",
|
||||
jsx: "jsx",
|
||||
@@ -258,7 +258,7 @@ function getLanguageExtension(filename: string) {
|
||||
return language ? loadLanguage(language) : null;
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
// Format file size
|
||||
function formatFileSize(bytes?: number): string {
|
||||
if (!bytes) return "Unknown size";
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
@@ -294,31 +294,31 @@ export function FileViewer({
|
||||
|
||||
const fileTypeInfo = getFileType(file.name);
|
||||
|
||||
// 文件大小限制 - 移除硬限制,支持大文件处理
|
||||
const WARNING_SIZE = 50 * 1024 * 1024; // 50MB 警告
|
||||
const MAX_SIZE = Number.MAX_SAFE_INTEGER; // 移除硬限制
|
||||
// File size limits - remove hard limits, support large file handling
|
||||
const WARNING_SIZE = 50 * 1024 * 1024; // 50MB warning
|
||||
const MAX_SIZE = Number.MAX_SAFE_INTEGER; // Remove hard limits
|
||||
|
||||
// 检查是否应该显示为文本
|
||||
// Check if should display as text
|
||||
const shouldShowAsText =
|
||||
fileTypeInfo.type === "text" ||
|
||||
fileTypeInfo.type === "code" ||
|
||||
(fileTypeInfo.type === "unknown" &&
|
||||
(forceShowAsText || !file.size || file.size <= WARNING_SIZE));
|
||||
|
||||
// 检查文件是否过大
|
||||
// Check if file is too large
|
||||
const isLargeFile = file.size && file.size > WARNING_SIZE;
|
||||
const isTooLarge = file.size && file.size > MAX_SIZE;
|
||||
|
||||
// 同步外部内容更改
|
||||
// Sync external content changes
|
||||
useEffect(() => {
|
||||
setEditedContent(content);
|
||||
// 只有在savedContent更新时才更新originalContent
|
||||
// Only update originalContent when savedContent is updated
|
||||
if (savedContent) {
|
||||
setOriginalContent(savedContent);
|
||||
}
|
||||
setHasChanges(content !== (savedContent || content));
|
||||
|
||||
// 如果是未知文件类型且文件较大,显示警告
|
||||
// If unknown file type and file is large, show warning
|
||||
if (fileTypeInfo.type === "unknown" && isLargeFile && !forceShowAsText) {
|
||||
setShowLargeFileWarning(true);
|
||||
} else {
|
||||
@@ -326,27 +326,27 @@ export function FileViewer({
|
||||
}
|
||||
}, [content, savedContent, fileTypeInfo.type, isLargeFile, forceShowAsText]);
|
||||
|
||||
// 处理内容更改
|
||||
// Handle content changes
|
||||
const handleContentChange = (newContent: string) => {
|
||||
setEditedContent(newContent);
|
||||
setHasChanges(newContent !== originalContent);
|
||||
onContentChange?.(newContent);
|
||||
};
|
||||
|
||||
// 保存文件
|
||||
// Save file
|
||||
const handleSave = () => {
|
||||
onSave?.(editedContent);
|
||||
// 注意:不在这里更新originalContent,因为它会通过savedContent prop更新
|
||||
// Note: Don't update originalContent here, as it will be updated via savedContent prop
|
||||
};
|
||||
|
||||
// 复原文件
|
||||
// Revert file
|
||||
const handleRevert = () => {
|
||||
setEditedContent(originalContent);
|
||||
setHasChanges(false);
|
||||
onContentChange?.(originalContent);
|
||||
};
|
||||
|
||||
// 搜索匹配功能
|
||||
// Search matching functionality
|
||||
const findMatches = (text: string) => {
|
||||
if (!text) {
|
||||
setSearchMatches([]);
|
||||
@@ -363,7 +363,7 @@ export function FileViewer({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
});
|
||||
// 避免无限循环
|
||||
// Avoid infinite loop
|
||||
if (match.index === regex.lastIndex) regex.lastIndex++;
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ export function FileViewer({
|
||||
setCurrentMatchIndex(matches.length > 0 ? 0 : -1);
|
||||
};
|
||||
|
||||
// 搜索导航
|
||||
// Search navigation
|
||||
const goToNextMatch = () => {
|
||||
if (searchMatches.length === 0) return;
|
||||
setCurrentMatchIndex((prev) => (prev + 1) % searchMatches.length);
|
||||
@@ -384,7 +384,7 @@ export function FileViewer({
|
||||
);
|
||||
};
|
||||
|
||||
// 替换功能
|
||||
// Replace functionality
|
||||
const handleFindReplace = (
|
||||
findText: string,
|
||||
replaceWithText: string,
|
||||
@@ -399,7 +399,7 @@ export function FileViewer({
|
||||
replaceWithText,
|
||||
);
|
||||
} else if (currentMatchIndex >= 0 && searchMatches[currentMatchIndex]) {
|
||||
// 替换当前匹配项
|
||||
// Replace current match
|
||||
const match = searchMatches[currentMatchIndex];
|
||||
newContent =
|
||||
editedContent.substring(0, match.start) +
|
||||
@@ -411,7 +411,7 @@ export function FileViewer({
|
||||
setHasChanges(newContent !== originalContent);
|
||||
onContentChange?.(newContent);
|
||||
|
||||
// 重新搜索以更新匹配项
|
||||
// Re-search to update matches
|
||||
setTimeout(() => findMatches(findText), 0);
|
||||
};
|
||||
|
||||
@@ -425,7 +425,7 @@ export function FileViewer({
|
||||
setShowReplacePanel(true);
|
||||
};
|
||||
|
||||
// 渲染带高亮的文本
|
||||
// Render highlighted text
|
||||
const renderHighlightedText = (text: string) => {
|
||||
if (!searchText || searchMatches.length === 0) {
|
||||
return text;
|
||||
@@ -435,12 +435,12 @@ export function FileViewer({
|
||||
let lastIndex = 0;
|
||||
|
||||
searchMatches.forEach((match, index) => {
|
||||
// 添加匹配前的文本
|
||||
// Add text before match
|
||||
if (match.start > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, match.start));
|
||||
}
|
||||
|
||||
// 添加高亮的匹配文本
|
||||
// Add highlighted match text
|
||||
const isCurrentMatch = index === currentMatchIndex;
|
||||
parts.push(
|
||||
<span
|
||||
@@ -459,7 +459,7 @@ export function FileViewer({
|
||||
lastIndex = match.end;
|
||||
});
|
||||
|
||||
// 添加最后的文本
|
||||
// Add final text
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex));
|
||||
}
|
||||
@@ -467,13 +467,13 @@ export function FileViewer({
|
||||
return parts;
|
||||
};
|
||||
|
||||
// 处理用户确认打开大文件
|
||||
// Handle user confirmation to open large file
|
||||
const handleConfirmOpenAsText = () => {
|
||||
setForceShowAsText(true);
|
||||
setShowLargeFileWarning(false);
|
||||
};
|
||||
|
||||
// 处理用户拒绝打开大文件
|
||||
// Handle user rejection to open large file
|
||||
const handleCancelOpenAsText = () => {
|
||||
setShowLargeFileWarning(false);
|
||||
};
|
||||
@@ -491,7 +491,7 @@ export function FileViewer({
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
{/* 文件信息头部 */}
|
||||
{/* File info header */}
|
||||
<div className="flex-shrink-0 bg-card border-b border-border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -517,7 +517,7 @@ export function FileViewer({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 编辑工具栏 - 直接显示,无需切换 */}
|
||||
{/* Edit toolbar - display directly, no toggle needed */}
|
||||
{isEditable && (
|
||||
<>
|
||||
<Button
|
||||
@@ -575,7 +575,7 @@ export function FileViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索和替换面板 */}
|
||||
{/* Search and replace panel */}
|
||||
{showSearchPanel && (
|
||||
<div className="flex-shrink-0 bg-muted/30 border-b border-border p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
@@ -657,9 +657,9 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文件内容 */}
|
||||
{/* File content */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{/* 大文件警告对话框 */}
|
||||
{/* Large file warning dialog */}
|
||||
{showLargeFileWarning && (
|
||||
<div className="h-full flex items-center justify-center bg-background">
|
||||
<div className="bg-card border border-destructive/30 rounded-lg p-6 max-w-md mx-4 shadow-lg">
|
||||
@@ -722,7 +722,7 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 图片预览 */}
|
||||
{/* Image preview */}
|
||||
{fileTypeInfo.type === "image" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
<img
|
||||
@@ -737,16 +737,16 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文本和代码文件预览 */}
|
||||
{/* Text and code file preview */}
|
||||
{shouldShowAsText && !showLargeFileWarning && (
|
||||
<div className="h-full flex flex-col">
|
||||
{fileTypeInfo.type === "code" ? (
|
||||
// 代码文件使用CodeMirror
|
||||
// Code files use CodeMirror
|
||||
<div className="h-full">
|
||||
{searchText && searchMatches.length > 0 ? (
|
||||
// 当有搜索结果时,显示只读的高亮文本(带行号)
|
||||
// When there are search results, show read-only highlighted text (with line numbers)
|
||||
<div className="h-full flex bg-muted">
|
||||
{/* 行号列 */}
|
||||
{/* Line number column */}
|
||||
<div className="flex-shrink-0 bg-muted border-r border-border px-2 py-4 text-xs text-muted-foreground font-mono select-none">
|
||||
{editedContent.split("\n").map((_, index) => (
|
||||
<div
|
||||
@@ -757,13 +757,13 @@ export function FileViewer({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 代码内容 */}
|
||||
{/* Code content */}
|
||||
<div className="flex-1 p-4 font-mono text-sm whitespace-pre-wrap overflow-auto text-foreground">
|
||||
{renderHighlightedText(editedContent)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 没有搜索时显示CodeMirror编辑器
|
||||
// Show CodeMirror editor when no search
|
||||
<CodeMirror
|
||||
value={editedContent}
|
||||
onChange={(value) => handleContentChange(value)}
|
||||
@@ -790,17 +790,17 @@ export function FileViewer({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 普通文本文件
|
||||
// Plain text files
|
||||
<div className="h-full">
|
||||
{isEditable ? (
|
||||
<div className="h-full">
|
||||
{searchText && searchMatches.length > 0 ? (
|
||||
// 当有搜索结果时,显示只读的高亮文本
|
||||
// When there are search results, show read-only highlighted text
|
||||
<div className="h-full p-4 font-mono text-sm whitespace-pre-wrap overflow-auto bg-background text-foreground">
|
||||
{renderHighlightedText(editedContent)}
|
||||
</div>
|
||||
) : (
|
||||
// 直接显示可编辑的textarea
|
||||
// Directly show editable textarea
|
||||
<textarea
|
||||
value={editedContent}
|
||||
onChange={(e) => handleContentChange(e.target.value)}
|
||||
@@ -811,7 +811,7 @@ export function FileViewer({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 只有非可编辑文件(媒体文件)才显示为只读
|
||||
// Only show as read-only for non-editable files (media files)
|
||||
<div className="h-full p-4 font-mono text-sm whitespace-pre-wrap overflow-auto bg-background text-foreground">
|
||||
{editedContent || content || "File is empty"}
|
||||
</div>
|
||||
@@ -821,7 +821,7 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 视频文件预览 */}
|
||||
{/* Video file preview */}
|
||||
{fileTypeInfo.type === "video" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
<video
|
||||
@@ -834,7 +834,7 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 音频文件预览 */}
|
||||
{/* Audio file preview */}
|
||||
{fileTypeInfo.type === "audio" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
@@ -857,7 +857,7 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 未知文件类型 - 只在不能显示为文本且没有警告时显示 */}
|
||||
{/* Unknown file type - only show when cannot display as text and no warning */}
|
||||
{fileTypeInfo.type === "unknown" &&
|
||||
!shouldShowAsText &&
|
||||
!showLargeFileWarning && (
|
||||
@@ -886,7 +886,7 @@ export function FileViewer({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部状态栏 */}
|
||||
{/* Bottom status bar */}
|
||||
<div className="flex-shrink-0 bg-muted/50 border-t border-border px-4 py-2 text-xs text-muted-foreground">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>{file.path}</span>
|
||||
|
||||
@@ -43,7 +43,7 @@ interface FileWindowProps {
|
||||
sshHost: SSHHost;
|
||||
initialX?: number;
|
||||
initialY?: number;
|
||||
// readOnly参数已移除,由FileViewer内部根据文件类型决定
|
||||
// readOnly parameter removed, determined internally by FileViewer based on file type
|
||||
}
|
||||
|
||||
export function FileWindow({
|
||||
@@ -71,17 +71,17 @@ export function FileWindow({
|
||||
|
||||
const currentWindow = windows.find((w) => w.id === windowId);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
const ensureSSHConnection = async () => {
|
||||
try {
|
||||
// 首先检查SSH连接状态
|
||||
// First check SSH connection status
|
||||
const status = await getSSHStatus(sshSessionId);
|
||||
console.log("SSH connection status:", status);
|
||||
|
||||
if (!status.connected) {
|
||||
console.log("SSH not connected, attempting to reconnect...");
|
||||
|
||||
// 重新建立连接
|
||||
// Re-establish connection
|
||||
await connectSSH(sshSessionId, {
|
||||
hostId: sshHost.id,
|
||||
ip: sshHost.ip,
|
||||
@@ -99,12 +99,12 @@ export function FileWindow({
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("SSH connection check/reconnect failed:", error);
|
||||
// 即使连接失败也尝试继续,让具体的API调用报错
|
||||
// Even if connection fails, try to continue and let specific API calls handle errors
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载文件内容
|
||||
// Load file content
|
||||
useEffect(() => {
|
||||
const loadFileContent = async () => {
|
||||
if (file.type !== "file") return;
|
||||
@@ -112,23 +112,23 @@ export function FileWindow({
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
const response = await readSSHFile(sshSessionId, file.path);
|
||||
const fileContent = response.content || "";
|
||||
setContent(fileContent);
|
||||
setPendingContent(fileContent); // 初始化待保存内容
|
||||
setPendingContent(fileContent); // Initialize pending content
|
||||
|
||||
// 如果文件大小未知,根据内容计算大小
|
||||
// If file size is unknown, calculate size based on content
|
||||
if (!file.size) {
|
||||
const contentSize = new Blob([fileContent]).size;
|
||||
file.size = contentSize;
|
||||
}
|
||||
|
||||
// 根据文件类型决定是否可编辑:除了媒体文件,其他都可编辑
|
||||
// Determine if editable based on file type: all except media files are editable
|
||||
const mediaExtensions = [
|
||||
// 图片文件
|
||||
// Image files
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
@@ -138,7 +138,7 @@ export function FileWindow({
|
||||
"webp",
|
||||
"tiff",
|
||||
"ico",
|
||||
// 音频文件
|
||||
// Audio files
|
||||
"mp3",
|
||||
"wav",
|
||||
"ogg",
|
||||
@@ -146,7 +146,7 @@ export function FileWindow({
|
||||
"flac",
|
||||
"m4a",
|
||||
"wma",
|
||||
// 视频文件
|
||||
// Video files
|
||||
"mp4",
|
||||
"avi",
|
||||
"mov",
|
||||
@@ -155,7 +155,7 @@ export function FileWindow({
|
||||
"mkv",
|
||||
"webm",
|
||||
"m4v",
|
||||
// 压缩文件
|
||||
// Archive files
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
@@ -163,7 +163,7 @@ export function FileWindow({
|
||||
"gz",
|
||||
"bz2",
|
||||
"xz",
|
||||
// 二进制文件
|
||||
// Binary files
|
||||
"exe",
|
||||
"dll",
|
||||
"so",
|
||||
@@ -173,12 +173,12 @@ export function FileWindow({
|
||||
];
|
||||
|
||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||||
// 只有媒体文件和二进制文件不可编辑,其他所有文件都可编辑
|
||||
// Only media files and binary files are not editable, all other files are editable
|
||||
setIsEditable(!mediaExtensions.includes(extension || ""));
|
||||
} catch (error: any) {
|
||||
console.error("Failed to load file:", error);
|
||||
|
||||
// 检查是否是大文件错误
|
||||
// Check if it's a large file error
|
||||
const errorData = error?.response?.data;
|
||||
if (errorData?.tooLarge) {
|
||||
toast.error(`File too large: ${errorData.error}`, {
|
||||
@@ -188,7 +188,7 @@ export function FileWindow({
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
) {
|
||||
// 如果是连接错误,提供更明确的错误信息
|
||||
// If connection error, provide more specific error message
|
||||
toast.error(
|
||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||
);
|
||||
@@ -205,19 +205,19 @@ export function FileWindow({
|
||||
loadFileContent();
|
||||
}, [file, sshSessionId, sshHost]);
|
||||
|
||||
// 保存文件
|
||||
// Save file
|
||||
const handleSave = async (newContent: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
await writeSSHFile(sshSessionId, file.path, newContent);
|
||||
setContent(newContent);
|
||||
setPendingContent(""); // 清除待保存内容
|
||||
setPendingContent(""); // Clear pending content
|
||||
|
||||
// 清除自动保存定时器
|
||||
// Clear auto-save timer
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current);
|
||||
autoSaveTimerRef.current = null;
|
||||
@@ -227,7 +227,7 @@ export function FileWindow({
|
||||
} catch (error: any) {
|
||||
console.error("Failed to save file:", error);
|
||||
|
||||
// 如果是连接错误,提供更明确的错误信息
|
||||
// If it's a connection error, provide more specific error message
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
@@ -243,16 +243,16 @@ export function FileWindow({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理内容变更 - 设置1分钟自动保存
|
||||
// Handle content changes - set 1-minute auto-save
|
||||
const handleContentChange = (newContent: string) => {
|
||||
setPendingContent(newContent);
|
||||
|
||||
// 清除之前的定时器
|
||||
// Clear previous timer
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current);
|
||||
}
|
||||
|
||||
// 设置新的1分钟自动保存定时器
|
||||
// Set new 1-minute auto-save timer
|
||||
autoSaveTimerRef.current = setTimeout(async () => {
|
||||
try {
|
||||
console.log("Auto-saving file...");
|
||||
@@ -262,10 +262,10 @@ export function FileWindow({
|
||||
console.error("Auto-save failed:", error);
|
||||
toast.error(t("fileManager.autoSaveFailed"));
|
||||
}
|
||||
}, 60000); // 1分钟 = 60000毫秒
|
||||
}, 60000); // 1 minute = 60000 milliseconds
|
||||
};
|
||||
|
||||
// 清理定时器
|
||||
// Cleanup timer
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoSaveTimerRef.current) {
|
||||
@@ -274,10 +274,10 @@ export function FileWindow({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 下载文件
|
||||
// Download file
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
// 确保SSH连接有效
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
const response = await downloadSSHFile(sshSessionId, file.path);
|
||||
@@ -308,7 +308,7 @@ export function FileWindow({
|
||||
} catch (error: any) {
|
||||
console.error("Failed to download file:", error);
|
||||
|
||||
// 如果是连接错误,提供更明确的错误信息
|
||||
// If it's a connection error, provide more specific error message
|
||||
if (
|
||||
error.message?.includes("connection") ||
|
||||
error.message?.includes("established")
|
||||
@@ -324,7 +324,7 @@ export function FileWindow({
|
||||
}
|
||||
};
|
||||
|
||||
// 窗口操作处理
|
||||
// Window operation handling
|
||||
const handleClose = () => {
|
||||
closeWindow(windowId);
|
||||
};
|
||||
@@ -366,7 +366,7 @@ export function FileWindow({
|
||||
content={pendingContent || content}
|
||||
savedContent={content}
|
||||
isLoading={isLoading}
|
||||
isEditable={isEditable} // 移除强制只读模式,由FileViewer内部控制
|
||||
isEditable={isEditable} // Remove forced read-only mode, controlled internally by FileViewer
|
||||
onContentChange={handleContentChange}
|
||||
onSave={(newContent) => handleSave(newContent)}
|
||||
onDownload={handleDownload}
|
||||
|
||||
@@ -35,13 +35,13 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
const nextZIndex = useRef(1000);
|
||||
const windowCounter = useRef(0);
|
||||
|
||||
// 打开新窗口
|
||||
// Open new window
|
||||
const openWindow = useCallback(
|
||||
(windowData: Omit<WindowInstance, "id" | "zIndex">) => {
|
||||
const id = `window-${++windowCounter.current}`;
|
||||
const zIndex = ++nextZIndex.current;
|
||||
|
||||
// 计算偏移位置,避免窗口完全重叠
|
||||
// Calculate offset position to avoid windows completely overlapping
|
||||
const offset = (windows.length % 5) * 30;
|
||||
const adjustedX = windowData.x + offset;
|
||||
const adjustedY = windowData.y + offset;
|
||||
@@ -60,12 +60,12 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
[windows.length],
|
||||
);
|
||||
|
||||
// 关闭窗口
|
||||
// Close window
|
||||
const closeWindow = useCallback((id: string) => {
|
||||
setWindows((prev) => prev.filter((w) => w.id !== id));
|
||||
}, []);
|
||||
|
||||
// 最小化窗口
|
||||
// Minimize window
|
||||
const minimizeWindow = useCallback((id: string) => {
|
||||
setWindows((prev) =>
|
||||
prev.map((w) =>
|
||||
@@ -74,7 +74,7 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 最大化/还原窗口
|
||||
// Maximize/restore window
|
||||
const maximizeWindow = useCallback((id: string) => {
|
||||
setWindows((prev) =>
|
||||
prev.map((w) =>
|
||||
@@ -83,7 +83,7 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 聚焦窗口 (置于顶层)
|
||||
// Focus window (bring to top)
|
||||
const focusWindow = useCallback((id: string) => {
|
||||
setWindows((prev) => {
|
||||
const targetWindow = prev.find((w) => w.id === id);
|
||||
@@ -94,7 +94,7 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 更新窗口属性
|
||||
// Update window properties
|
||||
const updateWindow = useCallback(
|
||||
(id: string, updates: Partial<WindowInstance>) => {
|
||||
setWindows((prev) =>
|
||||
@@ -117,7 +117,7 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
return (
|
||||
<WindowManagerContext.Provider value={contextValue}>
|
||||
{children}
|
||||
{/* 渲染所有窗口 */}
|
||||
{/* Render all windows */}
|
||||
<div className="window-container">
|
||||
{windows.map((window) => (
|
||||
<div key={window.id}>
|
||||
|
||||
Reference in New Issue
Block a user