Implement draggable file windows - Windows Explorer style

Added comprehensive draggable window system with the following features:

🪟 **DraggableWindow Component**:
- Full drag and drop functionality with title bar dragging
- Window resizing from all edges and corners
- Maximize/minimize/close window controls
- Double-click title bar to maximize/restore
- Auto position adjustment to prevent off-screen windows
- Windows-style blue gradient title bar

📁 **FileViewer Component**:
- Multi-format file support (text, code, images, videos, audio)
- Syntax highlighting distinction for code files
- Editable text files with real-time content tracking
- File metadata display (size, modified date, permissions)
- Save and download functionality
- Unsaved changes indicator

🎯 **WindowManager System**:
- Multi-window support with proper z-index management
- Window factory pattern for dynamic component creation
- Focus management - clicking brings window to front
- Smart window positioning with auto-offset
- Memory leak prevention with proper cleanup

🔗 **FileWindow Integration**:
- SSH file loading with error handling
- Auto-detect editable file types
- Real-time file saving to remote server
- Download files as binary blobs
- Loading states and progress feedback

 **User Experience**:
- Double-click any file to open in draggable window
- Multiple files can be open simultaneously
- Windows behave like native Windows Explorer
- Smooth animations and transitions
- Responsive design that works on all screen sizes

This transforms the file manager from a basic browser into a full
desktop-class application with native OS window management behavior.
This commit is contained in:
ZacharyZcR
2025-09-16 17:22:21 +08:00
parent 501de06266
commit 22ac7d8596
5 changed files with 882 additions and 3 deletions

View File

@@ -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<string>('');
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 (
<DraggableWindow
title={file.name}
initialX={initialX}
initialY={initialY}
initialWidth={800}
initialHeight={600}
minWidth={400}
minHeight={300}
onClose={handleClose}
onMinimize={handleMinimize}
onMaximize={handleMaximize}
onFocus={handleFocus}
isMaximized={currentWindow.isMaximized}
zIndex={currentWindow.zIndex}
>
<FileViewer
file={file}
content={content}
isLoading={isLoading}
isEditable={isEditable}
onSave={handleSave}
onDownload={handleDownload}
/>
</DraggableWindow>
);
}