diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx index bf24a58c..6b38e88d 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx @@ -3,6 +3,8 @@ import { FileManagerGrid } from "./FileManagerGrid"; import { FileManagerContextMenu } from "./FileManagerContextMenu"; import { useFileSelection } from "./hooks/useFileSelection"; import { useDragAndDrop } from "./hooks/useDragAndDrop"; +import { WindowManager, useWindowManager } from "./components/WindowManager"; +import { FileWindow } from "./components/FileWindow"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; @@ -47,7 +49,9 @@ interface FileManagerModernProps { onClose?: () => void; } -export function FileManagerModern({ initialHost, onClose }: FileManagerModernProps) { +// 内部组件,使用窗口管理器 +function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { + const { openWindow } = useWindowManager(); const { t } = useTranslation(); // State @@ -329,8 +333,38 @@ export function FileManagerModern({ initialHost, onClose }: FileManagerModernPro if (file.type === 'directory') { setCurrentPath(file.path); } else { - // 打开文件编辑器或预览 - console.log("Open file:", file); + // 在新窗口中打开文件 + if (!sshSessionId) { + toast.error(t("fileManager.noSSHConnection")); + return; + } + + // 计算窗口位置(稍微错开) + const windowCount = Date.now() % 10; // 简单的偏移计算 + const offsetX = 120 + (windowCount * 30); + const offsetY = 120 + (windowCount * 30); + + // 创建窗口组件工厂函数 + const createWindowComponent = (windowId: string) => ( + + ); + + openWindow({ + title: file.name, + x: offsetX, + y: offsetY, + width: 800, + height: 600, + isMaximized: false, + isMinimized: false, + component: createWindowComponent + }); } } @@ -538,4 +572,13 @@ export function FileManagerModern({ initialHost, onClose }: FileManagerModernPro ); +} + +// 主要的导出组件,包装了 WindowManager +export function FileManagerModern({ initialHost, onClose }: FileManagerModernProps) { + return ( + + + + ); } \ No newline at end of file diff --git a/src/ui/Desktop/Apps/File Manager/components/DraggableWindow.tsx b/src/ui/Desktop/Apps/File Manager/components/DraggableWindow.tsx new file mode 100644 index 00000000..7a30a806 --- /dev/null +++ b/src/ui/Desktop/Apps/File Manager/components/DraggableWindow.tsx @@ -0,0 +1,272 @@ +import React, { useState, useRef, useCallback, useEffect } from 'react'; +import { cn } from '@/lib/utils'; +import { Minus, Square, X, Maximize2, Minimize2 } from 'lucide-react'; + +interface DraggableWindowProps { + title: string; + children: React.ReactNode; + initialX?: number; + initialY?: number; + initialWidth?: number; + initialHeight?: number; + minWidth?: number; + minHeight?: number; + onClose: () => void; + onMinimize?: () => void; + onMaximize?: () => void; + isMaximized?: boolean; + zIndex?: number; + onFocus?: () => void; +} + +export function DraggableWindow({ + title, + children, + initialX = 100, + initialY = 100, + initialWidth = 600, + initialHeight = 400, + minWidth = 300, + minHeight = 200, + onClose, + onMinimize, + onMaximize, + isMaximized = false, + zIndex = 1000, + onFocus +}: DraggableWindowProps) { + // 窗口状态 + const [position, setPosition] = useState({ x: initialX, y: initialY }); + const [size, setSize] = useState({ width: initialWidth, height: initialHeight }); + const [isDragging, setIsDragging] = useState(false); + const [isResizing, setIsResizing] = useState(false); + const [resizeDirection, setResizeDirection] = useState(''); + + // 拖拽开始位置 + const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const [windowStart, setWindowStart] = useState({ x: 0, y: 0 }); + + const windowRef = useRef(null); + const titleBarRef = useRef(null); + + // 处理窗口焦点 + const handleWindowClick = useCallback(() => { + onFocus?.(); + }, [onFocus]); + + // 拖拽处理 + const handleMouseDown = useCallback((e: React.MouseEvent) => { + if (isMaximized) return; + + e.preventDefault(); + setIsDragging(true); + setDragStart({ x: e.clientX, y: e.clientY }); + setWindowStart({ x: position.x, y: position.y }); + onFocus?.(); + }, [isMaximized, position, onFocus]); + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (isDragging && !isMaximized) { + const deltaX = e.clientX - dragStart.x; + const deltaY = e.clientY - dragStart.y; + + setPosition({ + x: Math.max(0, Math.min(window.innerWidth - size.width, windowStart.x + deltaX)), + y: Math.max(0, Math.min(window.innerHeight - 40, windowStart.y + deltaY)) // 保持标题栏可见 + }); + } + + if (isResizing && !isMaximized) { + const deltaX = e.clientX - dragStart.x; + const deltaY = e.clientY - dragStart.y; + + let newWidth = size.width; + let newHeight = size.height; + let newX = position.x; + let newY = position.y; + + if (resizeDirection.includes('right')) { + newWidth = Math.max(minWidth, windowStart.x + deltaX); + } + if (resizeDirection.includes('left')) { + newWidth = Math.max(minWidth, size.width - deltaX); + newX = Math.min(windowStart.x + deltaX, position.x + size.width - minWidth); + } + if (resizeDirection.includes('bottom')) { + newHeight = Math.max(minHeight, windowStart.y + deltaY); + } + if (resizeDirection.includes('top')) { + newHeight = Math.max(minHeight, size.height - deltaY); + newY = Math.min(windowStart.y + deltaY, position.y + size.height - minHeight); + } + + setSize({ width: newWidth, height: newHeight }); + setPosition({ x: newX, y: newY }); + } + }, [isDragging, isResizing, isMaximized, dragStart, windowStart, size, position, minWidth, minHeight, resizeDirection]); + + const handleMouseUp = useCallback(() => { + setIsDragging(false); + setIsResizing(false); + setResizeDirection(''); + }, []); + + // 调整大小处理 + const handleResizeStart = useCallback((e: React.MouseEvent, direction: string) => { + if (isMaximized) return; + + e.preventDefault(); + e.stopPropagation(); + setIsResizing(true); + setResizeDirection(direction); + setDragStart({ x: e.clientX, y: e.clientY }); + setWindowStart({ x: size.width, y: size.height }); + onFocus?.(); + }, [isMaximized, size, onFocus]); + + // 全局事件监听 + useEffect(() => { + if (isDragging || isResizing) { + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + document.body.style.userSelect = 'none'; + document.body.style.cursor = isDragging ? 'grabbing' : 'resizing'; + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + document.body.style.userSelect = ''; + document.body.style.cursor = ''; + }; + } + }, [isDragging, isResizing, handleMouseMove, handleMouseUp]); + + // 双击标题栏最大化/还原 + const handleTitleDoubleClick = useCallback(() => { + onMaximize?.(); + }, [onMaximize]); + + return ( +
+ {/* 标题栏 */} +
+
+ {title} +
+ +
+ {onMinimize && ( + + )} + + {onMaximize && ( + + )} + + +
+
+ + {/* 窗口内容 */} +
+ {children} +
+ + {/* 调整大小边框 - 只在非最大化时显示 */} + {!isMaximized && ( + <> + {/* 边缘调整 */} +
handleResizeStart(e, 'top')} + /> +
handleResizeStart(e, 'bottom')} + /> +
handleResizeStart(e, 'left')} + /> +
handleResizeStart(e, 'right')} + /> + + {/* 角落调整 */} +
handleResizeStart(e, 'top-left')} + /> +
handleResizeStart(e, 'top-right')} + /> +
handleResizeStart(e, 'bottom-left')} + /> +
handleResizeStart(e, 'bottom-right')} + /> + + )} +
+ ); +} \ No newline at end of file diff --git a/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx b/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx new file mode 100644 index 00000000..006dd419 --- /dev/null +++ b/src/ui/Desktop/Apps/File Manager/components/FileViewer.tsx @@ -0,0 +1,266 @@ +import React, { useState, useEffect } from 'react'; +import { cn } from '@/lib/utils'; +import { + FileText, + Image as ImageIcon, + Film, + Music, + File as FileIcon, + Code, + AlertCircle, + Download, + Save +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface FileItem { + name: string; + type: "file" | "directory" | "link"; + path: string; + size?: number; + modified?: string; + permissions?: string; + owner?: string; + group?: string; +} + +interface FileViewerProps { + file: FileItem; + content?: string; + isLoading?: boolean; + isEditable?: boolean; + onContentChange?: (content: string) => void; + onSave?: (content: string) => void; + onDownload?: () => void; +} + +// 获取文件类型和图标 +function getFileType(filename: string): { type: string; icon: React.ReactNode; color: string } { + const ext = filename.split('.').pop()?.toLowerCase() || ''; + + const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp']; + const videoExts = ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm']; + const audioExts = ['mp3', 'wav', 'flac', 'ogg', 'aac', 'm4a']; + const textExts = ['txt', 'md', 'readme']; + const codeExts = ['js', 'ts', 'jsx', 'tsx', 'py', 'java', 'cpp', 'c', 'cs', 'php', 'rb', 'go', 'rs', 'html', 'css', 'scss', 'less', 'json', 'xml', 'yaml', 'yml', 'toml', 'ini', 'conf']; + + if (imageExts.includes(ext)) { + return { type: 'image', icon: , color: 'text-green-500' }; + } else if (videoExts.includes(ext)) { + return { type: 'video', icon: , color: 'text-purple-500' }; + } else if (audioExts.includes(ext)) { + return { type: 'audio', icon: , color: 'text-pink-500' }; + } else if (textExts.includes(ext)) { + return { type: 'text', icon: , color: 'text-blue-500' }; + } else if (codeExts.includes(ext)) { + return { type: 'code', icon: , color: 'text-yellow-500' }; + } else { + return { type: 'unknown', icon: , color: 'text-gray-500' }; + } +} + +// 格式化文件大小 +function formatFileSize(bytes?: number): string { + if (!bytes) return 'Unknown size'; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; +} + +export function FileViewer({ + file, + content = '', + isLoading = false, + isEditable = false, + onContentChange, + onSave, + onDownload +}: FileViewerProps) { + const [editedContent, setEditedContent] = useState(content); + const [hasChanges, setHasChanges] = useState(false); + + const fileTypeInfo = getFileType(file.name); + + // 同步外部内容更改 + useEffect(() => { + setEditedContent(content); + setHasChanges(false); + }, [content]); + + // 处理内容更改 + const handleContentChange = (newContent: string) => { + setEditedContent(newContent); + setHasChanges(newContent !== content); + onContentChange?.(newContent); + }; + + // 保存文件 + const handleSave = () => { + onSave?.(editedContent); + setHasChanges(false); + }; + + if (isLoading) { + return ( +
+
+
+

Loading file...

+
+
+ ); + } + + return ( +
+ {/* 文件信息头部 */} +
+
+
+
+ {fileTypeInfo.icon} +
+
+

{file.name}

+
+ {formatFileSize(file.size)} + {file.modified && Modified: {file.modified}} + + {fileTypeInfo.type.toUpperCase()} + +
+
+
+ +
+ {hasChanges && ( + + )} + {onDownload && ( + + )} +
+
+
+ + {/* 文件内容 */} +
+ {fileTypeInfo.type === 'image' && ( +
+ {file.name} { + (e.target as HTMLElement).style.display = 'none'; + // Show error message instead + }} + /> +
+ )} + + {(fileTypeInfo.type === 'text' || fileTypeInfo.type === 'code') && ( +
+ {isEditable ? ( +