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 (
+
+ );
+ }
+
+ return (
+
+ {/* 文件信息头部 */}
+
+
+
+
+ {fileTypeInfo.icon}
+
+
+
{file.name}
+
+ {formatFileSize(file.size)}
+ {file.modified && Modified: {file.modified} }
+
+ {fileTypeInfo.type.toUpperCase()}
+
+
+
+
+
+
+ {hasChanges && (
+
+
+ Save
+
+ )}
+ {onDownload && (
+
+
+ Download
+
+ )}
+
+
+
+
+ {/* 文件内容 */}
+
+ {fileTypeInfo.type === 'image' && (
+
+
{
+ (e.target as HTMLElement).style.display = 'none';
+ // Show error message instead
+ }}
+ />
+
+ )}
+
+ {(fileTypeInfo.type === 'text' || fileTypeInfo.type === 'code') && (
+
+ )}
+
+ {fileTypeInfo.type === 'video' && (
+
+
+ Your browser does not support video playback.
+
+
+ )}
+
+ {fileTypeInfo.type === 'audio' && (
+
+
+
+
+
+
+ Your browser does not support audio playback.
+
+
+
+ )}
+
+ {fileTypeInfo.type === 'unknown' && (
+
+
+
+
Cannot preview this file type
+
+ This file type is not supported for preview. You can download it to view in an external application.
+
+ {onDownload && (
+
+
+ Download File
+
+ )}
+
+
+ )}
+
+
+ {/* 底部状态栏 */}
+
+
+ {file.path}
+ {hasChanges && (
+ ● Unsaved changes
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/ui/Desktop/Apps/File Manager/components/FileWindow.tsx b/src/ui/Desktop/Apps/File Manager/components/FileWindow.tsx
new file mode 100644
index 00000000..9193e760
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/components/FileWindow.tsx
@@ -0,0 +1,166 @@
+import React, { useState, useEffect } from 'react';
+import { DraggableWindow } from './DraggableWindow';
+import { FileViewer } from './FileViewer';
+import { useWindowManager } from './WindowManager';
+import { downloadSSHFile, readSSHFile, writeSSHFile } from '@/ui/main-axios';
+import { toast } from 'sonner';
+
+interface FileItem {
+ name: string;
+ type: "file" | "directory" | "link";
+ path: string;
+ size?: number;
+ modified?: string;
+ permissions?: string;
+ owner?: string;
+ group?: string;
+}
+
+interface FileWindowProps {
+ windowId: string;
+ file: FileItem;
+ sshSessionId: string;
+ initialX?: number;
+ initialY?: number;
+}
+
+export function FileWindow({
+ windowId,
+ file,
+ sshSessionId,
+ initialX = 100,
+ initialY = 100
+}: FileWindowProps) {
+ const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, updateWindow, windows } = useWindowManager();
+
+ const [content, setContent] = useState
('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [isEditable, setIsEditable] = useState(false);
+
+ const currentWindow = windows.find(w => w.id === windowId);
+
+ // 加载文件内容
+ useEffect(() => {
+ const loadFileContent = async () => {
+ if (file.type !== 'file') return;
+
+ try {
+ setIsLoading(true);
+ const response = await readSSHFile(sshSessionId, file.path);
+ setContent(response.content || '');
+
+ // 根据文件类型决定是否可编辑
+ const editableExtensions = [
+ 'txt', 'md', 'js', 'ts', 'jsx', 'tsx', 'py', 'java', 'cpp', 'c', 'cs',
+ 'php', 'rb', 'go', 'rs', 'html', 'css', 'scss', 'less', 'json', 'xml',
+ 'yaml', 'yml', 'toml', 'ini', 'conf', 'sh', 'bat', 'ps1'
+ ];
+
+ const extension = file.name.split('.').pop()?.toLowerCase();
+ setIsEditable(editableExtensions.includes(extension || ''));
+ } catch (error: any) {
+ console.error('Failed to load file:', error);
+ toast.error(`Failed to load file: ${error.message || 'Unknown error'}`);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadFileContent();
+ }, [file, sshSessionId]);
+
+ // 保存文件
+ const handleSave = async (newContent: string) => {
+ try {
+ setIsLoading(true);
+ await writeSSHFile(sshSessionId, file.path, newContent);
+ setContent(newContent);
+ toast.success('File saved successfully');
+ } catch (error: any) {
+ console.error('Failed to save file:', error);
+ toast.error(`Failed to save file: ${error.message || 'Unknown error'}`);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 下载文件
+ const handleDownload = async () => {
+ try {
+ const response = await downloadSSHFile(sshSessionId, file.path);
+
+ if (response?.content) {
+ // Convert base64 to blob and trigger download
+ const byteCharacters = atob(response.content);
+ const byteNumbers = new Array(byteCharacters.length);
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+ }
+ const byteArray = new Uint8Array(byteNumbers);
+ const blob = new Blob([byteArray], { type: response.mimeType || 'application/octet-stream' });
+
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = response.fileName || file.name;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+
+ toast.success('File downloaded successfully');
+ }
+ } catch (error: any) {
+ console.error('Failed to download file:', error);
+ toast.error(`Failed to download file: ${error.message || 'Unknown error'}`);
+ }
+ };
+
+ // 窗口操作处理
+ const handleClose = () => {
+ closeWindow(windowId);
+ };
+
+ const handleMinimize = () => {
+ minimizeWindow(windowId);
+ };
+
+ const handleMaximize = () => {
+ maximizeWindow(windowId);
+ };
+
+ const handleFocus = () => {
+ focusWindow(windowId);
+ };
+
+ if (!currentWindow) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/ui/Desktop/Apps/File Manager/components/WindowManager.tsx b/src/ui/Desktop/Apps/File Manager/components/WindowManager.tsx
new file mode 100644
index 00000000..7b5ca203
--- /dev/null
+++ b/src/ui/Desktop/Apps/File Manager/components/WindowManager.tsx
@@ -0,0 +1,132 @@
+import React, { useState, useCallback, useRef } from 'react';
+
+export interface WindowInstance {
+ id: string;
+ title: string;
+ component: React.ReactNode | ((windowId: string) => React.ReactNode);
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ isMaximized: boolean;
+ isMinimized: boolean;
+ zIndex: number;
+}
+
+interface WindowManagerProps {
+ children?: React.ReactNode;
+}
+
+interface WindowManagerContextType {
+ windows: WindowInstance[];
+ openWindow: (window: Omit) => string;
+ closeWindow: (id: string) => void;
+ minimizeWindow: (id: string) => void;
+ maximizeWindow: (id: string) => void;
+ focusWindow: (id: string) => void;
+ updateWindow: (id: string, updates: Partial) => void;
+}
+
+const WindowManagerContext = React.createContext(null);
+
+export function WindowManager({ children }: WindowManagerProps) {
+ const [windows, setWindows] = useState([]);
+ const nextZIndex = useRef(1000);
+ const windowCounter = useRef(0);
+
+ // 打开新窗口
+ const openWindow = useCallback((windowData: Omit) => {
+ const id = `window-${++windowCounter.current}`;
+ const zIndex = ++nextZIndex.current;
+
+ // 计算偏移位置,避免窗口完全重叠
+ const offset = (windows.length % 5) * 30;
+ const adjustedX = windowData.x + offset;
+ const adjustedY = windowData.y + offset;
+
+ const newWindow: WindowInstance = {
+ ...windowData,
+ id,
+ zIndex,
+ x: adjustedX,
+ y: adjustedY,
+ };
+
+ setWindows(prev => [...prev, newWindow]);
+ return id;
+ }, [windows.length]);
+
+ // 关闭窗口
+ const closeWindow = useCallback((id: string) => {
+ setWindows(prev => prev.filter(w => w.id !== id));
+ }, []);
+
+ // 最小化窗口
+ const minimizeWindow = useCallback((id: string) => {
+ setWindows(prev => prev.map(w =>
+ w.id === id ? { ...w, isMinimized: !w.isMinimized } : w
+ ));
+ }, []);
+
+ // 最大化/还原窗口
+ const maximizeWindow = useCallback((id: string) => {
+ setWindows(prev => prev.map(w =>
+ w.id === id ? { ...w, isMaximized: !w.isMaximized } : w
+ ));
+ }, []);
+
+ // 聚焦窗口 (置于顶层)
+ const focusWindow = useCallback((id: string) => {
+ setWindows(prev => {
+ const targetWindow = prev.find(w => w.id === id);
+ if (!targetWindow) return prev;
+
+ const newZIndex = ++nextZIndex.current;
+ return prev.map(w =>
+ w.id === id ? { ...w, zIndex: newZIndex } : w
+ );
+ });
+ }, []);
+
+ // 更新窗口属性
+ const updateWindow = useCallback((id: string, updates: Partial) => {
+ setWindows(prev => prev.map(w =>
+ w.id === id ? { ...w, ...updates } : w
+ ));
+ }, []);
+
+ const contextValue: WindowManagerContextType = {
+ windows,
+ openWindow,
+ closeWindow,
+ minimizeWindow,
+ maximizeWindow,
+ focusWindow,
+ updateWindow,
+ };
+
+ return (
+
+ {children}
+ {/* 渲染所有窗口 */}
+
+ {windows.map(window => (
+
+ {typeof window.component === 'function'
+ ? window.component(window.id)
+ : window.component}
+
+ ))}
+
+
+ );
+}
+
+// Hook for using window manager
+export function useWindowManager() {
+ const context = React.useContext(WindowManagerContext);
+ if (!context) {
+ throw new Error('useWindowManager must be used within a WindowManager');
+ }
+ return context;
+}
\ No newline at end of file