Code cleanup
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -107,7 +107,6 @@ export function FileManagerContextMenu({
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
|
||||
// Adjust menu position to avoid going off screen
|
||||
const adjustPosition = () => {
|
||||
const menuWidth = 200;
|
||||
const menuHeight = 300;
|
||||
@@ -130,13 +129,10 @@ export function FileManagerContextMenu({
|
||||
|
||||
adjustPosition();
|
||||
|
||||
// Delay adding event listeners to avoid capturing the click that triggered the menu
|
||||
let cleanupFn: (() => void) | null = null;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Click outside to close menu
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
// Check if click is inside menu
|
||||
const target = event.target as Element;
|
||||
const menuElement = document.querySelector("[data-context-menu]");
|
||||
|
||||
@@ -145,13 +141,11 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
};
|
||||
|
||||
// Right-click to close menu (Windows behavior)
|
||||
const handleRightClick = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Keyboard support
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
@@ -159,12 +153,10 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
};
|
||||
|
||||
// Close menu on window blur
|
||||
const handleBlur = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
// Close menu on scroll (Windows behavior)
|
||||
const handleScroll = () => {
|
||||
onClose();
|
||||
};
|
||||
@@ -175,7 +167,6 @@ export function FileManagerContextMenu({
|
||||
window.addEventListener("blur", handleBlur);
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
|
||||
// Set cleanup function
|
||||
cleanupFn = () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside, true);
|
||||
document.removeEventListener("contextmenu", handleRightClick);
|
||||
@@ -183,7 +174,7 @@ export function FileManagerContextMenu({
|
||||
window.removeEventListener("blur", handleBlur);
|
||||
window.removeEventListener("scroll", handleScroll, true);
|
||||
};
|
||||
}, 50); // 50ms delay to ensure we don't capture the click that triggered the menu
|
||||
}, 50);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
@@ -204,13 +195,9 @@ export function FileManagerContextMenu({
|
||||
(f) => f.type === "file" && f.executable,
|
||||
);
|
||||
|
||||
// Build menu items
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
if (isFileContext) {
|
||||
// Menu when files/folders are selected
|
||||
|
||||
// Open terminal function - supports files and folders
|
||||
if (onOpenTerminal) {
|
||||
const targetPath = isSingleFile
|
||||
? files[0].type === "directory"
|
||||
@@ -229,7 +216,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Run executable file function - only show for single executable files
|
||||
if (isSingleFile && hasExecutableFiles && onRunExecutable) {
|
||||
menuItems.push({
|
||||
icon: <Play className="w-4 h-4" />,
|
||||
@@ -239,7 +225,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Add separator (if above functions exist)
|
||||
if (
|
||||
onOpenTerminal ||
|
||||
(isSingleFile && hasExecutableFiles && onRunExecutable)
|
||||
@@ -247,7 +232,6 @@ export function FileManagerContextMenu({
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// Preview function
|
||||
if (hasFiles && onPreview) {
|
||||
menuItems.push({
|
||||
icon: <Eye className="w-4 h-4" />,
|
||||
@@ -257,7 +241,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Download function - use proper download handler
|
||||
if (hasFiles && onDownload) {
|
||||
menuItems.push({
|
||||
icon: <Download className="w-4 h-4" />,
|
||||
@@ -269,7 +252,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// PIN/UNPIN function - only show for single files
|
||||
if (isSingleFile && files[0].type === "file") {
|
||||
const isCurrentlyPinned = isPinned ? isPinned(files[0]) : false;
|
||||
|
||||
@@ -288,7 +270,6 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
// Add folder shortcut - only show for single folders
|
||||
if (isSingleFile && files[0].type === "directory" && onAddShortcut) {
|
||||
menuItems.push({
|
||||
icon: <Bookmark className="w-4 h-4" />,
|
||||
@@ -297,7 +278,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Add separator (if above functions exist)
|
||||
if (
|
||||
(hasFiles && (onPreview || onDragToDesktop)) ||
|
||||
(isSingleFile &&
|
||||
@@ -308,7 +288,6 @@ export function FileManagerContextMenu({
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// Rename function
|
||||
if (isSingleFile && onRename) {
|
||||
menuItems.push({
|
||||
icon: <Edit3 className="w-4 h-4" />,
|
||||
@@ -318,7 +297,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Copy function
|
||||
if (onCopy) {
|
||||
menuItems.push({
|
||||
icon: <Copy className="w-4 h-4" />,
|
||||
@@ -330,7 +308,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Cut function
|
||||
if (onCut) {
|
||||
menuItems.push({
|
||||
icon: <Scissors className="w-4 h-4" />,
|
||||
@@ -342,12 +319,10 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Add separator (if edit functions exist)
|
||||
if ((isSingleFile && onRename) || onCopy || onCut) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// Delete function
|
||||
if (onDelete) {
|
||||
menuItems.push({
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
@@ -360,12 +335,10 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Add separator (if delete function exists)
|
||||
if (onDelete) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// Properties function
|
||||
if (isSingleFile && onProperties) {
|
||||
menuItems.push({
|
||||
icon: <Info className="w-4 h-4" />,
|
||||
@@ -374,9 +347,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Empty area right-click menu
|
||||
|
||||
// Open terminal in current directory
|
||||
if (onOpenTerminal && currentPath) {
|
||||
menuItems.push({
|
||||
icon: <Terminal className="w-4 h-4" />,
|
||||
@@ -386,7 +356,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Upload function
|
||||
if (onUpload) {
|
||||
menuItems.push({
|
||||
icon: <Upload className="w-4 h-4" />,
|
||||
@@ -396,12 +365,10 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Add separator (if terminal or upload functions exist)
|
||||
if ((onOpenTerminal && currentPath) || onUpload) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// New folder
|
||||
if (onNewFolder) {
|
||||
menuItems.push({
|
||||
icon: <FolderPlus className="w-4 h-4" />,
|
||||
@@ -411,7 +378,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// New file
|
||||
if (onNewFile) {
|
||||
menuItems.push({
|
||||
icon: <FilePlus className="w-4 h-4" />,
|
||||
@@ -421,12 +387,10 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Add separator (if new functions exist)
|
||||
if (onNewFolder || onNewFile) {
|
||||
menuItems.push({ separator: true } as MenuItem);
|
||||
}
|
||||
|
||||
// Refresh function
|
||||
if (onRefresh) {
|
||||
menuItems.push({
|
||||
icon: <RefreshCw className="w-4 h-4" />,
|
||||
@@ -436,7 +400,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Paste function
|
||||
if (hasClipboard && onPaste) {
|
||||
menuItems.push({
|
||||
icon: <Clipboard className="w-4 h-4" />,
|
||||
@@ -447,15 +410,12 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out consecutive separators
|
||||
const filteredMenuItems = menuItems.filter((item, index) => {
|
||||
if (!item.separator) return true;
|
||||
|
||||
// If it's a separator, check if previous and next are also separators
|
||||
const prevItem = index > 0 ? menuItems[index - 1] : null;
|
||||
const nextItem = index < menuItems.length - 1 ? menuItems[index + 1] : null;
|
||||
|
||||
// If previous or next is a separator, filter out current separator
|
||||
if (prevItem?.separator || nextItem?.separator) {
|
||||
return false;
|
||||
}
|
||||
@@ -463,7 +423,6 @@ export function FileManagerContextMenu({
|
||||
return true;
|
||||
});
|
||||
|
||||
// Remove separators at beginning and end
|
||||
const finalMenuItems = filteredMenuItems.filter((item, index) => {
|
||||
if (!item.separator) return true;
|
||||
return index > 0 && index < filteredMenuItems.length - 1;
|
||||
@@ -471,10 +430,8 @@ export function FileManagerContextMenu({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Transparent overlay to capture click events */}
|
||||
<div className="fixed inset-0 z-[99990]" />
|
||||
|
||||
{/* Menu body */}
|
||||
<div
|
||||
data-context-menu
|
||||
className="fixed bg-dark-bg border border-dark-border rounded-lg shadow-xl min-w-[180px] max-w-[250px] z-[99995] overflow-hidden"
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Upload,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
MoreHorizontal,
|
||||
RefreshCw,
|
||||
ArrowUp,
|
||||
FileSymlink,
|
||||
@@ -26,20 +25,16 @@ import {
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { FileItem } from "../../../types/index.js";
|
||||
|
||||
// Linus-style data structure: separate creation intent from actual files
|
||||
interface CreateIntent {
|
||||
id: string;
|
||||
type: 'file' | 'directory';
|
||||
type: "file" | "directory";
|
||||
defaultName: string;
|
||||
currentName: string;
|
||||
}
|
||||
|
||||
// Format file size
|
||||
function formatFileSize(bytes?: number): string {
|
||||
// Handle undefined or null cases
|
||||
if (bytes === undefined || bytes === null) return "-";
|
||||
|
||||
// Display 0-byte files as "0 B"
|
||||
if (bytes === 0) return "0 B";
|
||||
|
||||
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||
@@ -51,7 +46,6 @@ function formatFileSize(bytes?: number): string {
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
// Display one decimal place for values less than 10, integers for values greater than 10
|
||||
const formattedSize =
|
||||
size < 10 && unitIndex > 0 ? size.toFixed(1) : Math.round(size).toString();
|
||||
|
||||
@@ -95,7 +89,6 @@ interface FileManagerGridProps {
|
||||
onSystemDragStart?: (files: FileItem[]) => void;
|
||||
onSystemDragEnd?: (e: DragEvent, files: FileItem[]) => void;
|
||||
hasClipboard?: boolean;
|
||||
// Linus-style creation intent props
|
||||
createIntent?: CreateIntent | null;
|
||||
onConfirmCreate?: (name: string) => void;
|
||||
onCancelCreate?: () => void;
|
||||
@@ -206,16 +199,12 @@ export function FileManagerGrid({
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
const [editingName, setEditingName] = useState("");
|
||||
|
||||
|
||||
|
||||
// Unified drag state management
|
||||
const [dragState, setDragState] = useState<DragState>({
|
||||
type: "none",
|
||||
files: [],
|
||||
counter: 0,
|
||||
});
|
||||
|
||||
// Global mouse move listener - for drag tooltip following
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseMove = (e: MouseEvent) => {
|
||||
if (dragState.type === "internal" && dragState.files.length > 0) {
|
||||
@@ -235,11 +224,9 @@ export function FileManagerGrid({
|
||||
|
||||
const editInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Set initial name when starting edit
|
||||
useEffect(() => {
|
||||
if (editingFile) {
|
||||
setEditingName(editingFile.name);
|
||||
// Delay focus to ensure DOM is updated
|
||||
setTimeout(() => {
|
||||
editInputRef.current?.focus();
|
||||
editInputRef.current?.select();
|
||||
@@ -247,7 +234,6 @@ export function FileManagerGrid({
|
||||
}
|
||||
}, [editingFile]);
|
||||
|
||||
// Handle edit confirmation
|
||||
const handleEditConfirm = () => {
|
||||
if (
|
||||
editingFile &&
|
||||
@@ -260,13 +246,11 @@ export function FileManagerGrid({
|
||||
onCancelEdit?.();
|
||||
};
|
||||
|
||||
// Handle edit cancellation
|
||||
const handleEditCancel = () => {
|
||||
setEditingName("");
|
||||
onCancelEdit?.();
|
||||
};
|
||||
|
||||
// Handle input key events
|
||||
const handleEditKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -277,9 +261,7 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// File drag handling function
|
||||
const handleFileDragStart = (e: React.DragEvent, file: FileItem) => {
|
||||
// If dragged file is selected, drag all selected files
|
||||
const filesToDrag = selectedFiles.includes(file) ? selectedFiles : [file];
|
||||
|
||||
setDragState({
|
||||
@@ -290,7 +272,6 @@ export function FileManagerGrid({
|
||||
mousePosition: { x: e.clientX, y: e.clientY },
|
||||
});
|
||||
|
||||
// Set drag data, add internal drag identifier
|
||||
const dragData = {
|
||||
type: "internal_files",
|
||||
files: filesToDrag.map((f) => f.path),
|
||||
@@ -303,7 +284,6 @@ export function FileManagerGrid({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Only set target when dragging to different file and not being dragged file
|
||||
if (
|
||||
dragState.type === "internal" &&
|
||||
!dragState.files.some((f) => f.path === targetFile.path)
|
||||
@@ -317,7 +297,6 @@ export function FileManagerGrid({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Clear drag target highlight
|
||||
if (dragState.target?.path === targetFile.path) {
|
||||
setDragState((prev) => ({ ...prev, target: undefined }));
|
||||
}
|
||||
@@ -332,46 +311,23 @@ export function FileManagerGrid({
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if dragging to self
|
||||
const isDroppingOnSelf = dragState.files.some(
|
||||
(f) => f.path === targetFile.path,
|
||||
);
|
||||
if (isDroppingOnSelf) {
|
||||
console.log("Ignoring drop on self");
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine drag behavior:
|
||||
// 1. File/folder drag to folder = move operation
|
||||
// 2. Single file drag to single file = diff comparison
|
||||
// 3. Other cases = invalid operation
|
||||
|
||||
if (targetFile.type === "directory") {
|
||||
// Move operation
|
||||
console.log(
|
||||
"Moving files to directory:",
|
||||
dragState.files.map((f) => f.name),
|
||||
"to",
|
||||
targetFile.name,
|
||||
);
|
||||
onFileDrop?.(dragState.files, targetFile);
|
||||
} else if (
|
||||
targetFile.type === "file" &&
|
||||
dragState.files.length === 1 &&
|
||||
dragState.files[0].type === "file"
|
||||
) {
|
||||
// Diff comparison operation
|
||||
console.log(
|
||||
"Comparing files:",
|
||||
dragState.files[0].name,
|
||||
"vs",
|
||||
targetFile.name,
|
||||
);
|
||||
onFileDiff?.(dragState.files[0], targetFile);
|
||||
} else {
|
||||
// Invalid operation, notify user
|
||||
console.log("Invalid drag operation");
|
||||
}
|
||||
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
@@ -381,7 +337,6 @@ export function FileManagerGrid({
|
||||
const draggedFiles = dragState.draggedFiles || [];
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
|
||||
// Trigger system-level drag end detection with dragged files
|
||||
onSystemDragEnd?.(e.nativeEvent, draggedFiles);
|
||||
};
|
||||
|
||||
@@ -398,17 +353,14 @@ export function FileManagerGrid({
|
||||
} | null>(null);
|
||||
const [justFinishedSelecting, setJustFinishedSelecting] = useState(false);
|
||||
|
||||
// Navigation history management
|
||||
const [navigationHistory, setNavigationHistory] = useState<string[]>([
|
||||
currentPath,
|
||||
]);
|
||||
const [historyIndex, setHistoryIndex] = useState(0);
|
||||
|
||||
// Path editing state
|
||||
const [isEditingPath, setIsEditingPath] = useState(false);
|
||||
const [editPathValue, setEditPathValue] = useState(currentPath);
|
||||
|
||||
// Update navigation history
|
||||
useEffect(() => {
|
||||
const lastPath = navigationHistory[historyIndex];
|
||||
if (currentPath !== lastPath) {
|
||||
@@ -419,7 +371,6 @@ export function FileManagerGrid({
|
||||
}
|
||||
}, [currentPath]);
|
||||
|
||||
// Navigation functions
|
||||
const goBack = () => {
|
||||
if (historyIndex > 0) {
|
||||
const newIndex = historyIndex - 1;
|
||||
@@ -447,7 +398,6 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// Path navigation
|
||||
const pathParts = currentPath.split("/").filter(Boolean);
|
||||
const navigateToPath = (index: number) => {
|
||||
if (index === -1) {
|
||||
@@ -458,7 +408,6 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// Path editing functionality
|
||||
const startEditingPath = () => {
|
||||
setEditPathValue(currentPath);
|
||||
setIsEditingPath(true);
|
||||
@@ -472,7 +421,6 @@ export function FileManagerGrid({
|
||||
const confirmEditingPath = () => {
|
||||
const trimmedPath = editPathValue.trim();
|
||||
if (trimmedPath) {
|
||||
// Ensure path starts with /
|
||||
const normalizedPath = trimmedPath.startsWith("/")
|
||||
? trimmedPath
|
||||
: "/" + trimmedPath;
|
||||
@@ -491,31 +439,26 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// Sync editPathValue with currentPath
|
||||
useEffect(() => {
|
||||
if (!isEditingPath) {
|
||||
setEditPathValue(currentPath);
|
||||
}
|
||||
}, [currentPath, isEditingPath]);
|
||||
|
||||
// Drag and drop handling - distinguish internal file drag and external file upload
|
||||
const handleDragEnter = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Check if it's internal file drag
|
||||
const isInternalDrag = dragState.type === "internal";
|
||||
|
||||
if (!isInternalDrag) {
|
||||
// Only show upload prompt for external file drag
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
type: "external",
|
||||
counter: prev.counter + 1,
|
||||
}));
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||
// External drag detected
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -527,7 +470,6 @@ export function FileManagerGrid({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Check if it's internal file drag
|
||||
const isInternalDrag = dragState.type === "internal";
|
||||
|
||||
if (!isInternalDrag && dragState.type === "external") {
|
||||
@@ -549,11 +491,9 @@ export function FileManagerGrid({
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Check if it's internal file drag
|
||||
const isInternalDrag = dragState.type === "internal";
|
||||
|
||||
if (isInternalDrag) {
|
||||
// Update mouse position
|
||||
setDragState((prev) => ({
|
||||
...prev,
|
||||
mousePosition: { x: e.clientX, y: e.clientY },
|
||||
@@ -566,15 +506,11 @@ export function FileManagerGrid({
|
||||
[dragState.type],
|
||||
);
|
||||
|
||||
// Mouse wheel event handling, ensure scrolling works normally
|
||||
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||
// Don't prevent default scroll behavior, let browser handle scrolling
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
// Box selection functionality implementation
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
// Only start box selection in empty area, avoid interfering with file clicks
|
||||
if (e.target === e.currentTarget && e.button === 0) {
|
||||
e.preventDefault();
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
@@ -585,7 +521,6 @@ export function FileManagerGrid({
|
||||
setSelectionStart({ x: startX, y: startY });
|
||||
setSelectionRect({ x: startX, y: startY, width: 0, height: 0 });
|
||||
|
||||
// Reset flag for just completed selection, prepare for new selection
|
||||
setJustFinishedSelecting(false);
|
||||
}
|
||||
}, []);
|
||||
@@ -604,7 +539,6 @@ export function FileManagerGrid({
|
||||
|
||||
setSelectionRect({ x, y, width, height });
|
||||
|
||||
// Detect intersection with file items, perform real-time selection
|
||||
if (gridRef.current) {
|
||||
const fileElements =
|
||||
gridRef.current.querySelectorAll("[data-file-path]");
|
||||
@@ -614,7 +548,6 @@ export function FileManagerGrid({
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const containerRect = gridRef.current!.getBoundingClientRect();
|
||||
|
||||
// Simplify coordinate calculation - directly use coordinates relative to container
|
||||
const relativeElementRect = {
|
||||
left: elementRect.left - containerRect.left,
|
||||
top: elementRect.top - containerRect.top,
|
||||
@@ -622,7 +555,6 @@ export function FileManagerGrid({
|
||||
bottom: elementRect.bottom - containerRect.top,
|
||||
};
|
||||
|
||||
// Selection box coordinates
|
||||
const selectionBox = {
|
||||
left: x,
|
||||
top: y,
|
||||
@@ -630,7 +562,6 @@ export function FileManagerGrid({
|
||||
bottom: y + height,
|
||||
};
|
||||
|
||||
// Check if intersecting
|
||||
const intersects = !(
|
||||
relativeElementRect.right < selectionBox.left ||
|
||||
relativeElementRect.left > selectionBox.right ||
|
||||
@@ -642,21 +573,13 @@ export function FileManagerGrid({
|
||||
const filePath = element.getAttribute("data-file-path");
|
||||
if (filePath) {
|
||||
selectedPaths.push(filePath);
|
||||
console.log("Selected file:", filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("Total selected paths:", selectedPaths.length);
|
||||
|
||||
// Update selected files
|
||||
const newSelection = files.filter((file) =>
|
||||
selectedPaths.includes(file.path),
|
||||
);
|
||||
console.log(
|
||||
"New selection:",
|
||||
newSelection.map((f) => f.name),
|
||||
);
|
||||
onSelectionChange(newSelection);
|
||||
}
|
||||
}
|
||||
@@ -671,7 +594,6 @@ export function FileManagerGrid({
|
||||
setSelectionStart(null);
|
||||
setSelectionRect(null);
|
||||
|
||||
// Only consider as box selection when movement distance is large enough, otherwise it's a click
|
||||
const startPos = selectionStart;
|
||||
if (startPos) {
|
||||
const rect = gridRef.current?.getBoundingClientRect();
|
||||
@@ -683,13 +605,11 @@ export function FileManagerGrid({
|
||||
);
|
||||
|
||||
if (distance > 5) {
|
||||
// Real box selection, set flag to prevent immediate clearing
|
||||
setJustFinishedSelecting(true);
|
||||
setTimeout(() => {
|
||||
setJustFinishedSelecting(false);
|
||||
}, 50);
|
||||
} else {
|
||||
// Just a click, don't set flag, let handleGridClick handle normally
|
||||
setJustFinishedSelecting(false);
|
||||
}
|
||||
}
|
||||
@@ -699,7 +619,6 @@ export function FileManagerGrid({
|
||||
[isSelecting, selectionStart],
|
||||
);
|
||||
|
||||
// Global mouse event listener, ensure box selection can end outside container
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseUp = (e: MouseEvent) => {
|
||||
if (isSelecting) {
|
||||
@@ -707,7 +626,6 @@ export function FileManagerGrid({
|
||||
setSelectionStart(null);
|
||||
setSelectionRect(null);
|
||||
|
||||
// Global mouseup indicates drag box selection, set flag
|
||||
setJustFinishedSelecting(true);
|
||||
setTimeout(() => {
|
||||
setJustFinishedSelecting(false);
|
||||
@@ -747,58 +665,32 @@ export function FileManagerGrid({
|
||||
e.stopPropagation();
|
||||
|
||||
if (dragState.type === "internal") {
|
||||
// Internal drag to empty area: just cancel the drag operation
|
||||
console.log("Internal drag to empty area - cancelling drag operation");
|
||||
// Do not trigger download here - system drag end will handle it if truly outside window
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
} else if (dragState.type === "external") {
|
||||
// External drag: handle file upload
|
||||
if (onUpload && e.dataTransfer.files.length > 0) {
|
||||
onUpload(e.dataTransfer.files);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset drag state
|
||||
setDragState({ type: "none", files: [], counter: 0 });
|
||||
},
|
||||
[onUpload, onDownload, dragState],
|
||||
);
|
||||
|
||||
// File selection handling
|
||||
const handleFileClick = (file: FileItem, event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
// Ensure grid gets focus to support keyboard events
|
||||
if (gridRef.current) {
|
||||
gridRef.current.focus();
|
||||
}
|
||||
|
||||
console.log(
|
||||
"File clicked:",
|
||||
file.name,
|
||||
"Current selected:",
|
||||
selectedFiles.length,
|
||||
);
|
||||
|
||||
if (event.detail === 2) {
|
||||
// Double click to open
|
||||
console.log("Double click - opening file");
|
||||
onFileOpen(file);
|
||||
} else {
|
||||
// Single click to select
|
||||
const multiSelect = event.ctrlKey || event.metaKey;
|
||||
const rangeSelect = event.shiftKey;
|
||||
|
||||
console.log(
|
||||
"Single click - multiSelect:",
|
||||
multiSelect,
|
||||
"rangeSelect:",
|
||||
rangeSelect,
|
||||
);
|
||||
|
||||
if (rangeSelect && selectedFiles.length > 0) {
|
||||
// Range selection (Shift+click)
|
||||
console.log("Range selection");
|
||||
const lastSelected = selectedFiles[selectedFiles.length - 1];
|
||||
const currentIndex = files.findIndex((f) => f.path === file.path);
|
||||
const lastIndex = files.findIndex((f) => f.path === lastSelected.path);
|
||||
@@ -807,36 +699,26 @@ export function FileManagerGrid({
|
||||
const start = Math.min(currentIndex, lastIndex);
|
||||
const end = Math.max(currentIndex, lastIndex);
|
||||
const rangeFiles = files.slice(start, end + 1);
|
||||
console.log("Range selection result:", rangeFiles.length, "files");
|
||||
onSelectionChange(rangeFiles);
|
||||
}
|
||||
} else if (multiSelect) {
|
||||
// Multi-selection (Ctrl+click)
|
||||
console.log("Multi selection");
|
||||
const isSelected = selectedFiles.some((f) => f.path === file.path);
|
||||
if (isSelected) {
|
||||
console.log("Removing from selection");
|
||||
onSelectionChange(selectedFiles.filter((f) => f.path !== file.path));
|
||||
} else {
|
||||
console.log("Adding to selection");
|
||||
onSelectionChange([...selectedFiles, file]);
|
||||
}
|
||||
} else {
|
||||
// Single selection
|
||||
console.log("Single selection - should select only:", file.name);
|
||||
onSelectionChange([file]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Click empty area to cancel selection
|
||||
const handleGridClick = (event: React.MouseEvent) => {
|
||||
// Ensure grid gets focus to support keyboard events
|
||||
if (gridRef.current) {
|
||||
gridRef.current.focus();
|
||||
}
|
||||
|
||||
// If just completed box selection, don't clear selection
|
||||
if (
|
||||
event.target === event.currentTarget &&
|
||||
!isSelecting &&
|
||||
@@ -846,10 +728,8 @@ export function FileManagerGrid({
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard support
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
// Check if input box or editable element has focus, skip if so
|
||||
const activeElement = document.activeElement;
|
||||
if (
|
||||
activeElement &&
|
||||
@@ -868,7 +748,6 @@ export function FileManagerGrid({
|
||||
case "A":
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
console.log("Ctrl+A pressed - selecting all files:", files.length);
|
||||
onSelectionChange([...files]);
|
||||
}
|
||||
break;
|
||||
@@ -910,7 +789,6 @@ export function FileManagerGrid({
|
||||
break;
|
||||
case "Delete":
|
||||
if (selectedFiles.length > 0 && onDelete) {
|
||||
// Trigger delete operation
|
||||
onDelete(selectedFiles);
|
||||
}
|
||||
break;
|
||||
@@ -922,7 +800,7 @@ export function FileManagerGrid({
|
||||
break;
|
||||
case "y":
|
||||
case "Y":
|
||||
if ((event.ctrlKey || event.metaKey)) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
onRefresh();
|
||||
}
|
||||
@@ -957,9 +835,7 @@ export function FileManagerGrid({
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-dark-bg overflow-hidden">
|
||||
{/* Toolbar and path navigation */}
|
||||
<div className="flex-shrink-0 border-b border-dark-border">
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex items-center gap-1 p-2 border-b border-dark-border">
|
||||
<button
|
||||
onClick={goBack}
|
||||
@@ -1004,10 +880,8 @@ export function FileManagerGrid({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Breadcrumb navigation */}
|
||||
<div className="flex items-center px-3 py-2 text-sm">
|
||||
{isEditingPath ? (
|
||||
// Edit mode: path input box
|
||||
<div className="flex-1 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
@@ -1038,7 +912,6 @@ export function FileManagerGrid({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
// View mode: breadcrumb navigation
|
||||
<>
|
||||
<button
|
||||
onClick={() => navigateToPath(-1)}
|
||||
@@ -1071,7 +944,6 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main file grid - scroll area */}
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
<div
|
||||
ref={gridRef}
|
||||
@@ -1092,7 +964,6 @@ export function FileManagerGrid({
|
||||
onContextMenu={(e) => onContextMenu?.(e)}
|
||||
tabIndex={0}
|
||||
>
|
||||
{/* Drag hint overlay */}
|
||||
{dragState.type === "external" && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm z-10 pointer-events-none animate-in fade-in-0">
|
||||
<div className="text-center p-8 bg-background/95 border-2 border-dashed border-primary rounded-lg shadow-lg">
|
||||
@@ -1128,7 +999,6 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
|
||||
{/* Linus-style creation intent UI - pure separation */}
|
||||
{createIntent && (
|
||||
<CreateIntentGridItem
|
||||
intent={createIntent}
|
||||
@@ -1169,10 +1039,8 @@ export function FileManagerGrid({
|
||||
onDragEnd={handleFileDragEnd}
|
||||
>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{/* File icon */}
|
||||
<div className="mb-2">{getFileIcon(file, viewMode)}</div>
|
||||
|
||||
{/* File name */}
|
||||
<div className="w-full flex flex-col items-center">
|
||||
{editingFile?.path === file.path ? (
|
||||
<input
|
||||
@@ -1221,7 +1089,6 @@ export function FileManagerGrid({
|
||||
) : (
|
||||
/* List view */
|
||||
<div className="space-y-1">
|
||||
{/* Linus-style creation intent UI - list view */}
|
||||
{createIntent && (
|
||||
<CreateIntentListItem
|
||||
intent={createIntent}
|
||||
@@ -1260,12 +1127,10 @@ export function FileManagerGrid({
|
||||
onDrop={(e) => handleFileDrop(e, file)}
|
||||
onDragEnd={handleFileDragEnd}
|
||||
>
|
||||
{/* File icon */}
|
||||
<div className="flex-shrink-0">
|
||||
{getFileIcon(file, viewMode)}
|
||||
</div>
|
||||
|
||||
{/* File info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{editingFile?.path === file.path ? (
|
||||
<input
|
||||
@@ -1305,7 +1170,6 @@ export function FileManagerGrid({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File size */}
|
||||
<div className="flex-shrink-0 text-right">
|
||||
{file.type === "file" &&
|
||||
file.size !== undefined &&
|
||||
@@ -1316,7 +1180,6 @@ export function FileManagerGrid({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Permission info */}
|
||||
<div className="flex-shrink-0 text-right w-20">
|
||||
{file.permissions && (
|
||||
<p className="text-xs text-muted-foreground font-mono">
|
||||
@@ -1330,7 +1193,6 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection rectangle */}
|
||||
{isSelecting && selectionRect && (
|
||||
<div
|
||||
className="absolute pointer-events-none border-2 border-primary bg-primary/10 z-50"
|
||||
@@ -1345,7 +1207,6 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status bar */}
|
||||
<div className="flex-shrink-0 border-t border-dark-border px-4 py-2 text-xs text-muted-foreground">
|
||||
<div className="flex justify-between items-center">
|
||||
<span>{t("fileManager.itemCount", { count: files.length })}</span>
|
||||
@@ -1357,7 +1218,6 @@ export function FileManagerGrid({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Drag following tooltip - rendered as portal to ensure highest z-index */}
|
||||
{dragState.type === "internal" &&
|
||||
(dragState.files.length > 0 || dragState.draggedFiles?.length > 0) &&
|
||||
dragState.mousePosition &&
|
||||
@@ -1365,27 +1225,43 @@ export function FileManagerGrid({
|
||||
<div
|
||||
className="fixed pointer-events-none"
|
||||
style={{
|
||||
left: Math.min(Math.max(dragState.mousePosition.x + 40, 0), window.innerWidth - 300),
|
||||
top: Math.max(Math.min(dragState.mousePosition.y - 80, window.innerHeight - 100), 0),
|
||||
left: Math.min(
|
||||
Math.max(dragState.mousePosition.x + 40, 0),
|
||||
window.innerWidth - 300,
|
||||
),
|
||||
top: Math.max(
|
||||
Math.min(
|
||||
dragState.mousePosition.y - 80,
|
||||
window.innerHeight - 100,
|
||||
),
|
||||
0,
|
||||
),
|
||||
zIndex: 999999,
|
||||
}}
|
||||
>
|
||||
<div className="bg-background border border-border rounded-md shadow-md px-3 py-2 flex items-center gap-2">
|
||||
{(() => {
|
||||
const files = dragState.files.length > 0 ? dragState.files : dragState.draggedFiles || [];
|
||||
const files =
|
||||
dragState.files.length > 0
|
||||
? dragState.files
|
||||
: dragState.draggedFiles || [];
|
||||
return dragState.target ? (
|
||||
dragState.target.type === "directory" ? (
|
||||
<>
|
||||
<Move className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{t("fileManager.moveTo", { name: dragState.target.name })}
|
||||
{t("fileManager.moveTo", {
|
||||
name: dragState.target.name,
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<GitCompare className="w-4 h-4 text-purple-500" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{t("fileManager.diffCompareWith", { name: dragState.target.name })}
|
||||
{t("fileManager.diffCompareWith", {
|
||||
name: dragState.target.name,
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
@@ -1393,20 +1269,21 @@ export function FileManagerGrid({
|
||||
<>
|
||||
<Download className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{t("fileManager.dragOutsideToDownload", { count: files.length })}
|
||||
{t("fileManager.dragOutsideToDownload", {
|
||||
count: files.length,
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Linus-style creation intent component: Grid view
|
||||
function CreateIntentGridItem({
|
||||
intent,
|
||||
onConfirm,
|
||||
@@ -1439,7 +1316,7 @@ function CreateIntentGridItem({
|
||||
<div className="group p-3 rounded-lg border-2 border-dashed border-primary bg-primary/10">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="mb-2">
|
||||
{intent.type === 'directory' ? (
|
||||
{intent.type === "directory" ? (
|
||||
<Folder className="w-8 h-8 text-primary" />
|
||||
) : (
|
||||
<File className="w-8 h-8 text-primary" />
|
||||
@@ -1453,14 +1330,17 @@ function CreateIntentGridItem({
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => onConfirm?.(inputName.trim())}
|
||||
className="w-full max-w-[120px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-xs text-center text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px] outline-none"
|
||||
placeholder={intent.type === 'directory' ? t('fileManager.folderName') : t('fileManager.fileName')}
|
||||
placeholder={
|
||||
intent.type === "directory"
|
||||
? t("fileManager.folderName")
|
||||
: t("fileManager.fileName")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Linus-style creation intent component: List view
|
||||
function CreateIntentListItem({
|
||||
intent,
|
||||
onConfirm,
|
||||
@@ -1492,7 +1372,7 @@ function CreateIntentListItem({
|
||||
return (
|
||||
<div className="flex items-center gap-3 p-2 rounded border-2 border-dashed border-primary bg-primary/10">
|
||||
<div className="flex-shrink-0">
|
||||
{intent.type === 'directory' ? (
|
||||
{intent.type === "directory" ? (
|
||||
<Folder className="w-6 h-6 text-primary" />
|
||||
) : (
|
||||
<File className="w-6 h-6 text-primary" />
|
||||
@@ -1506,7 +1386,11 @@ function CreateIntentListItem({
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => onConfirm?.(inputName.trim())}
|
||||
className="flex-1 min-w-0 max-w-[200px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-2 py-1 text-sm text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px] outline-none"
|
||||
placeholder={intent.type === 'directory' ? t('fileManager.folderName') : t('fileManager.fileName')}
|
||||
placeholder={
|
||||
intent.type === "directory"
|
||||
? t("fileManager.folderName")
|
||||
: t("fileManager.fileName")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -38,9 +38,9 @@ interface FileManagerSidebarProps {
|
||||
currentPath: string;
|
||||
onPathChange: (path: string) => void;
|
||||
onLoadDirectory?: (path: string) => void;
|
||||
onFileOpen?: (file: SidebarItem) => void; // Added: handle file opening
|
||||
onFileOpen?: (file: SidebarItem) => void;
|
||||
sshSessionId?: string;
|
||||
refreshTrigger?: number; // Used to trigger data refresh
|
||||
refreshTrigger?: number;
|
||||
}
|
||||
|
||||
export function FileManagerSidebar({
|
||||
@@ -61,7 +61,6 @@ export function FileManagerSidebar({
|
||||
new Set(["root"]),
|
||||
);
|
||||
|
||||
// Right-click menu state
|
||||
const [contextMenu, setContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -74,12 +73,10 @@ export function FileManagerSidebar({
|
||||
item: null,
|
||||
});
|
||||
|
||||
// Load quick access data
|
||||
useEffect(() => {
|
||||
loadQuickAccessData();
|
||||
}, [currentHost, refreshTrigger]);
|
||||
|
||||
// Load directory tree (depends on sshSessionId)
|
||||
useEffect(() => {
|
||||
if (sshSessionId) {
|
||||
loadDirectoryTree();
|
||||
@@ -90,7 +87,6 @@ export function FileManagerSidebar({
|
||||
if (!currentHost?.id) return;
|
||||
|
||||
try {
|
||||
// Load recent files (limit to 5)
|
||||
const recentData = await getRecentFiles(currentHost.id);
|
||||
const recentItems = recentData.slice(0, 5).map((item: any) => ({
|
||||
id: `recent-${item.id}`,
|
||||
@@ -101,7 +97,6 @@ export function FileManagerSidebar({
|
||||
}));
|
||||
setRecentItems(recentItems);
|
||||
|
||||
// Load pinned files
|
||||
const pinnedData = await getPinnedFiles(currentHost.id);
|
||||
const pinnedItems = pinnedData.map((item: any) => ({
|
||||
id: `pinned-${item.id}`,
|
||||
@@ -111,7 +106,6 @@ export function FileManagerSidebar({
|
||||
}));
|
||||
setPinnedItems(pinnedItems);
|
||||
|
||||
// Load folder shortcuts
|
||||
const shortcutData = await getFolderShortcuts(currentHost.id);
|
||||
const shortcutItems = shortcutData.map((item: any) => ({
|
||||
id: `shortcut-${item.id}`,
|
||||
@@ -122,20 +116,18 @@ export function FileManagerSidebar({
|
||||
setShortcuts(shortcutItems);
|
||||
} catch (error) {
|
||||
console.error("Failed to load quick access data:", error);
|
||||
// If loading fails, keep empty arrays
|
||||
setRecentItems([]);
|
||||
setPinnedItems([]);
|
||||
setShortcuts([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete functionality implementation
|
||||
const handleRemoveRecentFile = async (item: SidebarItem) => {
|
||||
if (!currentHost?.id) return;
|
||||
|
||||
try {
|
||||
await removeRecentFile(currentHost.id, item.path);
|
||||
loadQuickAccessData(); // Reload data
|
||||
loadQuickAccessData();
|
||||
toast.success(
|
||||
t("fileManager.removedFromRecentFiles", { name: item.name }),
|
||||
);
|
||||
@@ -150,7 +142,7 @@ export function FileManagerSidebar({
|
||||
|
||||
try {
|
||||
await removePinnedFile(currentHost.id, item.path);
|
||||
loadQuickAccessData(); // Reload data
|
||||
loadQuickAccessData();
|
||||
toast.success(t("fileManager.unpinnedSuccessfully", { name: item.name }));
|
||||
} catch (error) {
|
||||
console.error("Failed to unpin file:", error);
|
||||
@@ -163,7 +155,7 @@ export function FileManagerSidebar({
|
||||
|
||||
try {
|
||||
await removeFolderShortcut(currentHost.id, item.path);
|
||||
loadQuickAccessData(); // Reload data
|
||||
loadQuickAccessData();
|
||||
toast.success(t("fileManager.removedShortcut", { name: item.name }));
|
||||
} catch (error) {
|
||||
console.error("Failed to remove shortcut:", error);
|
||||
@@ -175,11 +167,10 @@ export function FileManagerSidebar({
|
||||
if (!currentHost?.id || recentItems.length === 0) return;
|
||||
|
||||
try {
|
||||
// Batch delete all recent files
|
||||
await Promise.all(
|
||||
recentItems.map((item) => removeRecentFile(currentHost.id, item.path)),
|
||||
);
|
||||
loadQuickAccessData(); // Reload data
|
||||
loadQuickAccessData();
|
||||
toast.success(t("fileManager.clearedAllRecentFiles"));
|
||||
} catch (error) {
|
||||
console.error("Failed to clear recent files:", error);
|
||||
@@ -187,7 +178,6 @@ export function FileManagerSidebar({
|
||||
}
|
||||
};
|
||||
|
||||
// Right-click menu handling
|
||||
const handleContextMenu = (e: React.MouseEvent, item: SidebarItem) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -204,7 +194,6 @@ export function FileManagerSidebar({
|
||||
setContextMenu((prev) => ({ ...prev, isVisible: false, item: null }));
|
||||
};
|
||||
|
||||
// Click outside to close menu
|
||||
useEffect(() => {
|
||||
if (!contextMenu.isVisible) return;
|
||||
|
||||
@@ -223,7 +212,6 @@ export function FileManagerSidebar({
|
||||
}
|
||||
};
|
||||
|
||||
// Delay adding listeners to avoid immediate trigger
|
||||
const timeoutId = setTimeout(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
@@ -240,10 +228,8 @@ export function FileManagerSidebar({
|
||||
if (!sshSessionId) return;
|
||||
|
||||
try {
|
||||
// Load root directory
|
||||
const response = await listSSHFiles(sshSessionId, "/");
|
||||
|
||||
// listSSHFiles now always returns {files: Array, path: string} format
|
||||
const rootFiles = response.files || [];
|
||||
const rootFolders = rootFiles.filter(
|
||||
(item: any) => item.type === "directory",
|
||||
@@ -255,7 +241,7 @@ export function FileManagerSidebar({
|
||||
path: folder.path,
|
||||
type: "folder" as const,
|
||||
isExpanded: false,
|
||||
children: [], // Subdirectories will be loaded on demand
|
||||
children: [],
|
||||
}));
|
||||
|
||||
setDirectoryTree([
|
||||
@@ -270,7 +256,6 @@ export function FileManagerSidebar({
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Failed to load directory tree:", error);
|
||||
// If loading fails, show simple root directory
|
||||
setDirectoryTree([
|
||||
{
|
||||
id: "root",
|
||||
@@ -289,17 +274,14 @@ export function FileManagerSidebar({
|
||||
toggleFolder(item.id, item.path);
|
||||
onPathChange(item.path);
|
||||
} else if (item.type === "recent" || item.type === "pinned") {
|
||||
// For file types, call file open callback
|
||||
if (onFileOpen) {
|
||||
onFileOpen(item);
|
||||
} else {
|
||||
// If no file open callback, switch to file directory
|
||||
const directory =
|
||||
item.path.substring(0, item.path.lastIndexOf("/")) || "/";
|
||||
onPathChange(directory);
|
||||
}
|
||||
} else if (item.type === "shortcut") {
|
||||
// Folder shortcuts directly switch to directory
|
||||
onPathChange(item.path);
|
||||
}
|
||||
};
|
||||
@@ -312,12 +294,10 @@ export function FileManagerSidebar({
|
||||
} else {
|
||||
newExpanded.add(folderId);
|
||||
|
||||
// Load subdirectories on demand
|
||||
if (sshSessionId && folderPath && folderPath !== "/") {
|
||||
try {
|
||||
const subResponse = await listSSHFiles(sshSessionId, folderPath);
|
||||
|
||||
// listSSHFiles now always returns {files: Array, path: string} format
|
||||
const subFiles = subResponse.files || [];
|
||||
const subFolders = subFiles.filter(
|
||||
(item: any) => item.type === "directory",
|
||||
@@ -332,7 +312,6 @@ export function FileManagerSidebar({
|
||||
children: [],
|
||||
}));
|
||||
|
||||
// Update directory tree, add subdirectories for current folder
|
||||
setDirectoryTree((prevTree) => {
|
||||
const updateChildren = (items: SidebarItem[]): SidebarItem[] => {
|
||||
return items.map((item) => {
|
||||
@@ -370,7 +349,6 @@ export function FileManagerSidebar({
|
||||
style={{ paddingLeft: `${12 + level * 16}px`, paddingRight: "12px" }}
|
||||
onClick={() => handleItemClick(item)}
|
||||
onContextMenu={(e) => {
|
||||
// Only quick access items need right-click menu
|
||||
if (
|
||||
item.type === "recent" ||
|
||||
item.type === "pinned" ||
|
||||
@@ -438,7 +416,6 @@ export function FileManagerSidebar({
|
||||
);
|
||||
};
|
||||
|
||||
// Check if there are any quick access items
|
||||
const hasQuickAccessItems =
|
||||
recentItems.length > 0 || pinnedItems.length > 0 || shortcuts.length > 0;
|
||||
|
||||
@@ -447,7 +424,6 @@ export function FileManagerSidebar({
|
||||
<div className="h-full flex flex-col bg-dark-bg border-r border-dark-border">
|
||||
<div className="flex-1 relative overflow-hidden">
|
||||
<div className="absolute inset-1.5 overflow-y-auto thin-scrollbar space-y-4">
|
||||
{/* Quick access area */}
|
||||
{renderSection(
|
||||
t("fileManager.recent"),
|
||||
<Clock className="w-3 h-3" />,
|
||||
@@ -464,7 +440,6 @@ export function FileManagerSidebar({
|
||||
shortcuts,
|
||||
)}
|
||||
|
||||
{/* Directory tree */}
|
||||
<div
|
||||
className={cn(
|
||||
hasQuickAccessItems && "pt-4 border-t border-dark-border",
|
||||
@@ -482,7 +457,6 @@ export function FileManagerSidebar({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right-click menu */}
|
||||
{contextMenu.isVisible && contextMenu.item && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" />
|
||||
|
||||
@@ -33,8 +33,6 @@ export function DiffViewer({
|
||||
file2,
|
||||
sshSessionId,
|
||||
sshHost,
|
||||
onDownload1,
|
||||
onDownload2,
|
||||
}: DiffViewerProps) {
|
||||
const { t } = useTranslation();
|
||||
const [content1, setContent1] = useState<string>("");
|
||||
@@ -46,7 +44,6 @@ export function DiffViewer({
|
||||
);
|
||||
const [showLineNumbers, setShowLineNumbers] = useState(true);
|
||||
|
||||
// Ensure SSH connection is valid
|
||||
const ensureSSHConnection = async () => {
|
||||
try {
|
||||
const status = await getSSHStatus(sshSessionId);
|
||||
@@ -70,7 +67,6 @@ export function DiffViewer({
|
||||
}
|
||||
};
|
||||
|
||||
// Load file contents
|
||||
const loadFileContents = async () => {
|
||||
if (file1.type !== "file" || file2.type !== "file") {
|
||||
setError(t("fileManager.canOnlyCompareFiles"));
|
||||
@@ -81,10 +77,8 @@ export function DiffViewer({
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 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),
|
||||
@@ -106,13 +100,16 @@ export function DiffViewer({
|
||||
t("fileManager.sshConnectionFailed", {
|
||||
name: sshHost.name,
|
||||
ip: sshHost.ip,
|
||||
port: sshHost.port
|
||||
port: sshHost.port,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
setError(
|
||||
t("fileManager.loadFileFailed", {
|
||||
error: error.message || errorData?.error || t("fileManager.unknownError")
|
||||
error:
|
||||
error.message ||
|
||||
errorData?.error ||
|
||||
t("fileManager.unknownError"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -121,7 +118,6 @@ export function DiffViewer({
|
||||
}
|
||||
};
|
||||
|
||||
// Download file
|
||||
const handleDownloadFile = async (file: FileItem) => {
|
||||
try {
|
||||
await ensureSSHConnection();
|
||||
@@ -147,15 +143,20 @@ export function DiffViewer({
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.success(t("fileManager.downloadFileSuccess", { name: file.name }));
|
||||
toast.success(
|
||||
t("fileManager.downloadFileSuccess", { name: file.name }),
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Failed to download file:", error);
|
||||
toast.error(t("fileManager.downloadFileFailed") + ": " + (error.message || t("fileManager.unknownError")));
|
||||
toast.error(
|
||||
t("fileManager.downloadFileFailed") +
|
||||
": " +
|
||||
(error.message || t("fileManager.unknownError")),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Get file language type
|
||||
const getFileLanguage = (fileName: string): string => {
|
||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||
const languageMap: Record<string, string> = {
|
||||
@@ -190,7 +191,6 @@ export function DiffViewer({
|
||||
return languageMap[ext || ""] || "plaintext";
|
||||
};
|
||||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
loadFileContents();
|
||||
}, [file1, file2, sshSessionId]);
|
||||
@@ -200,7 +200,9 @@ 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">{t("fileManager.loadingFileComparison")}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("fileManager.loadingFileComparison")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -223,12 +225,13 @@ 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">{t("fileManager.compare")}:</span>
|
||||
<span className="text-muted-foreground">
|
||||
{t("fileManager.compare")}:
|
||||
</span>
|
||||
<span className="font-medium text-green-400 mx-2">
|
||||
{file1.name}
|
||||
</span>
|
||||
@@ -238,7 +241,6 @@ export function DiffViewer({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* View toggle */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -248,10 +250,11 @@ export function DiffViewer({
|
||||
)
|
||||
}
|
||||
>
|
||||
{diffMode === "side-by-side" ? t("fileManager.sideBySide") : t("fileManager.inline")}
|
||||
{diffMode === "side-by-side"
|
||||
? t("fileManager.sideBySide")
|
||||
: t("fileManager.inline")}
|
||||
</Button>
|
||||
|
||||
{/* Line number toggle */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -264,7 +267,6 @@ export function DiffViewer({
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{/* Download buttons */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -285,7 +287,6 @@ export function DiffViewer({
|
||||
{file2.name}
|
||||
</Button>
|
||||
|
||||
{/* Refresh button */}
|
||||
<Button variant="outline" size="sm" onClick={loadFileContents}>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</Button>
|
||||
@@ -293,7 +294,6 @@ export function DiffViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Diff editor */}
|
||||
<div className="flex-1">
|
||||
<DiffEditor
|
||||
original={content1}
|
||||
@@ -322,7 +322,9 @@ 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">{t("fileManager.initializingEditor")}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("fileManager.initializingEditor")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export function DiffWindow({
|
||||
|
||||
const currentWindow = windows.find((w) => w.id === windowId);
|
||||
|
||||
// Window operation handling
|
||||
const handleClose = () => {
|
||||
closeWindow(windowId);
|
||||
};
|
||||
@@ -49,7 +48,10 @@ export function DiffWindow({
|
||||
|
||||
return (
|
||||
<DraggableWindow
|
||||
title={t("fileManager.fileComparison", { file1: file1.name, file2: file2.name })}
|
||||
title={t("fileManager.fileComparison", {
|
||||
file1: file1.name,
|
||||
file2: file2.name,
|
||||
})}
|
||||
initialX={initialX}
|
||||
initialY={initialY}
|
||||
initialWidth={1200}
|
||||
|
||||
@@ -39,7 +39,6 @@ export function DraggableWindow({
|
||||
targetSize,
|
||||
}: DraggableWindowProps) {
|
||||
const { t } = useTranslation();
|
||||
// Window state
|
||||
const [position, setPosition] = useState({ x: initialX, y: initialY });
|
||||
const [size, setSize] = useState({
|
||||
width: initialWidth,
|
||||
@@ -49,7 +48,6 @@ export function DraggableWindow({
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [resizeDirection, setResizeDirection] = useState<string>("");
|
||||
|
||||
// Drag and resize start positions
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
||||
const [windowStart, setWindowStart] = useState({ x: 0, y: 0 });
|
||||
const [sizeStart, setSizeStart] = useState({ width: 0, height: 0 });
|
||||
@@ -57,17 +55,14 @@ export function DraggableWindow({
|
||||
const windowRef = useRef<HTMLDivElement>(null);
|
||||
const titleBarRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle target size changes for media files
|
||||
useEffect(() => {
|
||||
if (targetSize && !isMaximized) {
|
||||
const maxWidth = Math.min(window.innerWidth * 0.9, 1200);
|
||||
const maxHeight = Math.min(window.innerHeight * 0.8, 800);
|
||||
|
||||
// Calculate appropriate window size maintaining aspect ratio
|
||||
let newWidth = Math.min(targetSize.width + 50, maxWidth); // Add padding for UI
|
||||
let newHeight = Math.min(targetSize.height + 150, maxHeight); // Add padding for header/footer
|
||||
let newWidth = Math.min(targetSize.width + 50, maxWidth);
|
||||
let newHeight = Math.min(targetSize.height + 150, maxHeight);
|
||||
|
||||
// If still too large, scale down maintaining aspect ratio
|
||||
if (newWidth > maxWidth || newHeight > maxHeight) {
|
||||
const widthRatio = maxWidth / newWidth;
|
||||
const heightRatio = maxHeight / newHeight;
|
||||
@@ -77,26 +72,22 @@ export function DraggableWindow({
|
||||
newHeight = Math.floor(newHeight * scale);
|
||||
}
|
||||
|
||||
// Ensure minimum size
|
||||
newWidth = Math.max(newWidth, minWidth);
|
||||
newHeight = Math.max(newHeight, minHeight);
|
||||
|
||||
setSize({ width: newWidth, height: newHeight });
|
||||
|
||||
// Center the window
|
||||
setPosition({
|
||||
x: Math.max(0, (window.innerWidth - newWidth) / 2),
|
||||
y: Math.max(0, (window.innerHeight - newHeight) / 2)
|
||||
y: Math.max(0, (window.innerHeight - newHeight) / 2),
|
||||
});
|
||||
}
|
||||
}, [targetSize, isMaximized, minWidth, minHeight]);
|
||||
|
||||
// Handle window focus
|
||||
const handleWindowClick = useCallback(() => {
|
||||
onFocus?.();
|
||||
}, [onFocus]);
|
||||
|
||||
// Drag handling
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (isMaximized) return;
|
||||
@@ -119,44 +110,44 @@ export function DraggableWindow({
|
||||
const newX = windowStart.x + deltaX;
|
||||
const newY = windowStart.y + deltaY;
|
||||
|
||||
// Find the positioning container by checking parent hierarchy
|
||||
const windowElement = windowRef.current;
|
||||
let positioningContainer = null;
|
||||
let currentElement = windowElement?.parentElement;
|
||||
|
||||
|
||||
while (currentElement && currentElement !== document.body) {
|
||||
const computedStyle = window.getComputedStyle(currentElement);
|
||||
const position = computedStyle.position;
|
||||
const transform = computedStyle.transform;
|
||||
|
||||
if (position === 'relative' || position === 'absolute' || position === 'fixed' || transform !== 'none') {
|
||||
|
||||
if (
|
||||
position === "relative" ||
|
||||
position === "absolute" ||
|
||||
position === "fixed" ||
|
||||
transform !== "none"
|
||||
) {
|
||||
positioningContainer = currentElement;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
|
||||
// Calculate boundaries based on the actual positioning context
|
||||
let maxX, maxY, minX, minY;
|
||||
|
||||
|
||||
if (positioningContainer) {
|
||||
const containerRect = positioningContainer.getBoundingClientRect();
|
||||
|
||||
// Window is positioned relative to a positioning container
|
||||
|
||||
maxX = containerRect.width - size.width;
|
||||
maxY = containerRect.height - size.height;
|
||||
minX = 0;
|
||||
minY = 0;
|
||||
} else {
|
||||
// Window is positioned relative to viewport
|
||||
maxX = window.innerWidth - size.width;
|
||||
maxY = window.innerHeight - size.height;
|
||||
minX = 0;
|
||||
minY = 0;
|
||||
}
|
||||
|
||||
// Ensure window stays within boundaries
|
||||
const constrainedX = Math.max(minX, Math.min(maxX, newX));
|
||||
const constrainedY = Math.max(minY, Math.min(maxY, newY));
|
||||
|
||||
@@ -175,14 +166,12 @@ export function DraggableWindow({
|
||||
let newX = windowStart.x;
|
||||
let newY = windowStart.y;
|
||||
|
||||
// Handle horizontal resizing
|
||||
if (resizeDirection.includes("right")) {
|
||||
newWidth = Math.max(minWidth, sizeStart.width + deltaX);
|
||||
}
|
||||
if (resizeDirection.includes("left")) {
|
||||
const widthChange = -deltaX;
|
||||
newWidth = Math.max(minWidth, sizeStart.width + widthChange);
|
||||
// Only move position if we're actually changing size
|
||||
if (newWidth > minWidth || widthChange > 0) {
|
||||
newX = windowStart.x - (newWidth - sizeStart.width);
|
||||
} else {
|
||||
@@ -190,14 +179,12 @@ export function DraggableWindow({
|
||||
}
|
||||
}
|
||||
|
||||
// Handle vertical resizing
|
||||
if (resizeDirection.includes("bottom")) {
|
||||
newHeight = Math.max(minHeight, sizeStart.height + deltaY);
|
||||
}
|
||||
if (resizeDirection.includes("top")) {
|
||||
const heightChange = -deltaY;
|
||||
newHeight = Math.max(minHeight, sizeStart.height + heightChange);
|
||||
// Only move position if we're actually changing size
|
||||
if (newHeight > minHeight || heightChange > 0) {
|
||||
newY = windowStart.y - (newHeight - sizeStart.height);
|
||||
} else {
|
||||
@@ -205,7 +192,6 @@ export function DraggableWindow({
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure window stays within viewport
|
||||
newX = Math.max(0, Math.min(window.innerWidth - newWidth, newX));
|
||||
newY = Math.max(0, Math.min(window.innerHeight - newHeight, newY));
|
||||
|
||||
@@ -234,7 +220,6 @@ export function DraggableWindow({
|
||||
setResizeDirection("");
|
||||
}, []);
|
||||
|
||||
// Resize handling
|
||||
const handleResizeStart = useCallback(
|
||||
(e: React.MouseEvent, direction: string) => {
|
||||
if (isMaximized) return;
|
||||
@@ -251,7 +236,6 @@ export function DraggableWindow({
|
||||
[isMaximized, position, size, onFocus],
|
||||
);
|
||||
|
||||
// Global event listeners
|
||||
useEffect(() => {
|
||||
if (isDragging || isResizing) {
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
@@ -268,7 +252,6 @@ export function DraggableWindow({
|
||||
}
|
||||
}, [isDragging, isResizing, handleMouseMove, handleMouseUp]);
|
||||
|
||||
// Double-click title bar to maximize/restore
|
||||
const handleTitleDoubleClick = useCallback(() => {
|
||||
onMaximize?.();
|
||||
}, [onMaximize]);
|
||||
@@ -290,7 +273,6 @@ export function DraggableWindow({
|
||||
}}
|
||||
onClick={handleWindowClick}
|
||||
>
|
||||
{/* Title bar */}
|
||||
<div
|
||||
ref={titleBarRef}
|
||||
className={cn(
|
||||
@@ -349,7 +331,6 @@ export function DraggableWindow({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Window content */}
|
||||
<div
|
||||
className="flex-1 overflow-auto"
|
||||
style={{ height: "calc(100% - 40px)" }}
|
||||
@@ -357,10 +338,8 @@ 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")}
|
||||
@@ -378,7 +357,6 @@ 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")}
|
||||
|
||||
@@ -51,7 +51,12 @@ import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { loadLanguage } from "@uiw/codemirror-extensions-langs";
|
||||
import { EditorView, keymap } from "@codemirror/view";
|
||||
import { searchKeymap, search, openSearchPanel } from "@codemirror/search";
|
||||
import { defaultKeymap, history, historyKeymap, toggleComment } from "@codemirror/commands";
|
||||
import {
|
||||
defaultKeymap,
|
||||
history,
|
||||
historyKeymap,
|
||||
toggleComment,
|
||||
} from "@codemirror/commands";
|
||||
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
||||
import { PhotoProvider, PhotoView } from "react-photo-view";
|
||||
import "react-photo-view/dist/react-photo-view.css";
|
||||
@@ -64,8 +69,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark as syntaxTheme } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
|
||||
// Use local PDF.js worker to avoid CDN issues
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = "/pdf.worker.min.js";
|
||||
|
||||
interface FileItem {
|
||||
name: string;
|
||||
@@ -87,15 +91,16 @@ interface FileViewerProps {
|
||||
onContentChange?: (content: string) => void;
|
||||
onSave?: (content: string) => void;
|
||||
onDownload?: () => void;
|
||||
onMediaDimensionsChange?: (dimensions: { width: number; height: number }) => void;
|
||||
onMediaDimensionsChange?: (dimensions: {
|
||||
width: number;
|
||||
height: number;
|
||||
}) => 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" />;
|
||||
}
|
||||
@@ -141,7 +146,6 @@ 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;
|
||||
@@ -239,17 +243,14 @@ function getFileType(filename: string): {
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
@@ -288,7 +289,6 @@ function getLanguageExtension(filename: string) {
|
||||
return language ? loadLanguage(language) : null;
|
||||
}
|
||||
|
||||
// Format file size
|
||||
function formatFileSize(bytes?: number, t?: any): string {
|
||||
if (!bytes) return t ? t("fileManager.unknownSize") : "Unknown size";
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
@@ -328,32 +328,25 @@ export function FileViewer({
|
||||
|
||||
const fileTypeInfo = getFileType(file.name);
|
||||
|
||||
// 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
|
||||
const WARNING_SIZE = 50 * 1024 * 1024;
|
||||
const MAX_SIZE = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// 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);
|
||||
// Only update originalContent when savedContent is updated
|
||||
if (savedContent) {
|
||||
setOriginalContent(savedContent);
|
||||
}
|
||||
// Fix: Compare current content with saved content properly
|
||||
setHasChanges(content !== savedContent);
|
||||
|
||||
// If unknown file type and file is large, show warning
|
||||
if (fileTypeInfo.type === "unknown" && isLargeFile && !forceShowAsText) {
|
||||
setShowLargeFileWarning(true);
|
||||
} else {
|
||||
@@ -361,59 +354,46 @@ export function FileViewer({
|
||||
}
|
||||
}, [content, savedContent, fileTypeInfo.type, isLargeFile, forceShowAsText]);
|
||||
|
||||
// Handle content changes
|
||||
const handleContentChange = (newContent: string) => {
|
||||
setEditedContent(newContent);
|
||||
// Fix: Compare with savedContent instead of originalContent for consistency
|
||||
setHasChanges(newContent !== savedContent);
|
||||
onContentChange?.(newContent);
|
||||
};
|
||||
|
||||
// Save file
|
||||
const handleSave = () => {
|
||||
onSave?.(editedContent);
|
||||
// Note: Don't update originalContent here, as it will be updated via savedContent prop
|
||||
};
|
||||
|
||||
// Revert file
|
||||
const handleRevert = () => {
|
||||
setEditedContent(savedContent);
|
||||
setHasChanges(false);
|
||||
onContentChange?.(savedContent);
|
||||
};
|
||||
|
||||
// Handle save shortcut specifically
|
||||
useEffect(() => {
|
||||
if (!editorFocused || !isEditable) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Only handle Ctrl+S for custom save, let CodeMirror handle everything else
|
||||
const isCtrl = e.ctrlKey || e.metaKey;
|
||||
if (isCtrl && e.key.toLowerCase() === 's') {
|
||||
if (isCtrl && e.key.toLowerCase() === "s") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSave();
|
||||
}
|
||||
};
|
||||
|
||||
// Add event listener with capture for save shortcut only
|
||||
document.addEventListener('keydown', handleKeyDown, true);
|
||||
document.addEventListener("keydown", handleKeyDown, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown, true);
|
||||
document.removeEventListener("keydown", handleKeyDown, true);
|
||||
};
|
||||
}, [editorFocused, isEditable, handleSave]);
|
||||
|
||||
|
||||
|
||||
|
||||
// Handle user confirmation to open large file
|
||||
const handleConfirmOpenAsText = () => {
|
||||
setForceShowAsText(true);
|
||||
setShowLargeFileWarning(false);
|
||||
};
|
||||
|
||||
// Handle user rejection to open large file
|
||||
const handleCancelOpenAsText = () => {
|
||||
setShowLargeFileWarning(false);
|
||||
};
|
||||
@@ -431,7 +411,6 @@ 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">
|
||||
@@ -442,7 +421,11 @@ export function FileViewer({
|
||||
<h3 className="font-medium text-foreground">{file.name}</h3>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span>{formatFileSize(file.size, t)}</span>
|
||||
{file.modified && <span>{t("fileManager.modified")}: {file.modified}</span>}
|
||||
{file.modified && (
|
||||
<span>
|
||||
{t("fileManager.modified")}: {file.modified}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"px-2 py-1 rounded-full text-xs",
|
||||
@@ -457,13 +440,11 @@ export function FileViewer({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Search button */}
|
||||
{isEditable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
// Use CodeMirror's proper API to open search panel
|
||||
if (editorRef.current) {
|
||||
const view = editorRef.current.view;
|
||||
if (view) {
|
||||
@@ -477,7 +458,6 @@ export function FileViewer({
|
||||
<Search className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{/* Keyboard shortcuts help */}
|
||||
{isEditable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -526,11 +506,12 @@ export function FileViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Keyboard shortcuts help panel */}
|
||||
{showKeyboardShortcuts && isEditable && (
|
||||
<div className="flex-shrink-0 bg-muted/30 border-b border-border p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold">{t("fileManager.keyboardShortcuts")}</h3>
|
||||
<h3 className="text-sm font-semibold">
|
||||
{t("fileManager.keyboardShortcuts")}
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -542,60 +523,88 @@ export function FileViewer({
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-xs">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-muted-foreground">{t("fileManager.searchAndReplace")}</h4>
|
||||
<h4 className="font-medium text-muted-foreground">
|
||||
{t("fileManager.searchAndReplace")}
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.search")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Ctrl+F</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Ctrl+F
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.replace")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Ctrl+H</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Ctrl+H
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.findNext")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">F3</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
F3
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.findPrevious")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Shift+F3</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Shift+F3
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-muted-foreground">{t("fileManager.editing")}</h4>
|
||||
<h4 className="font-medium text-muted-foreground">
|
||||
{t("fileManager.editing")}
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.save")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Ctrl+S</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Ctrl+S
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.selectAll")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Ctrl+A</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Ctrl+A
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.undo")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Ctrl+Z</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Ctrl+Z
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.redo")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Ctrl+Y</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Ctrl+Y
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.toggleComment")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Ctrl+/</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Ctrl+/
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.autoComplete")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Ctrl+Space</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Ctrl+Space
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.moveLineUp")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Alt+↑</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Alt+↑
|
||||
</kbd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>{t("fileManager.moveLineDown")}</span>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">Alt+↓</kbd>
|
||||
<kbd className="px-2 py-1 bg-background rounded text-xs">
|
||||
Alt+↓
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -603,9 +612,7 @@ 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">
|
||||
@@ -616,7 +623,9 @@ export function FileViewer({
|
||||
{t("fileManager.largeFileWarning")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{t("fileManager.largeFileWarningDesc", { size: formatFileSize(file.size, t) })}
|
||||
{t("fileManager.largeFileWarningDesc", {
|
||||
size: formatFileSize(file.size, t),
|
||||
})}
|
||||
</p>
|
||||
{isTooLarge ? (
|
||||
<div className="bg-destructive/10 border border-destructive/30 rounded p-3 mb-4">
|
||||
@@ -667,19 +676,15 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image preview with react-photo-view */}
|
||||
{fileTypeInfo.type === "image" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full relative">
|
||||
{imageLoadError ? (
|
||||
// Error state
|
||||
<div className="text-center text-muted-foreground">
|
||||
<AlertCircle className="w-16 h-16 mx-auto mb-4 text-muted-foreground/50" />
|
||||
<h3 className="text-lg font-medium mb-2">
|
||||
{t("fileManager.imageLoadError")}
|
||||
</h3>
|
||||
<p className="text-sm mb-4">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-sm mb-4">{file.name}</p>
|
||||
{onDownload && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -703,12 +708,15 @@ export function FileViewer({
|
||||
setImageLoading(false);
|
||||
setImageLoadError(false);
|
||||
|
||||
// Get natural dimensions and notify parent
|
||||
const img = e.currentTarget;
|
||||
if (onMediaDimensionsChange && img.naturalWidth && img.naturalHeight) {
|
||||
if (
|
||||
onMediaDimensionsChange &&
|
||||
img.naturalWidth &&
|
||||
img.naturalHeight
|
||||
) {
|
||||
onMediaDimensionsChange({
|
||||
width: img.naturalWidth,
|
||||
height: img.naturalHeight
|
||||
height: img.naturalHeight,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@@ -721,23 +729,22 @@ export function FileViewer({
|
||||
</PhotoProvider>
|
||||
)}
|
||||
|
||||
{/* Loading state */}
|
||||
{imageLoading && !imageLoadError && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-background/80">
|
||||
<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">Loading image...</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Loading image...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Unified text and code file editor */}
|
||||
{shouldShowAsText && !showLargeFileWarning && (
|
||||
<div className="h-full flex flex-col">
|
||||
{isEditable ? (
|
||||
// Unified CodeMirror editor for all text-based files
|
||||
<CodeMirror
|
||||
ref={editorRef}
|
||||
value={editedContent}
|
||||
@@ -756,20 +763,18 @@ export function FileViewer({
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...completionKeymap,
|
||||
// Custom keybindings
|
||||
{
|
||||
key: "Mod-/",
|
||||
run: toggleComment,
|
||||
preventDefault: true
|
||||
preventDefault: true,
|
||||
},
|
||||
{
|
||||
key: "Mod-h",
|
||||
run: () => {
|
||||
// Let CodeMirror search handle this, just prevent browser default
|
||||
return false; // Return false to let search keymap handle it
|
||||
return false;
|
||||
},
|
||||
preventDefault: true
|
||||
}
|
||||
preventDefault: true,
|
||||
},
|
||||
]),
|
||||
EditorView.theme({
|
||||
"&": {
|
||||
@@ -800,7 +805,6 @@ export function FileViewer({
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// Read-only view for non-editable files
|
||||
<div className="h-full p-4 font-mono text-sm whitespace-pre-wrap overflow-auto bg-background text-foreground">
|
||||
{editedContent || content || t("fileManager.fileIsEmpty")}
|
||||
</div>
|
||||
@@ -808,22 +812,29 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Video file preview with enhanced HTML5 support */}
|
||||
{fileTypeInfo.type === "video" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
<div className="w-full max-w-4xl">
|
||||
{(() => {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase() || '';
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
||||
const mimeType = (() => {
|
||||
switch (ext) {
|
||||
case 'mp4': return 'video/mp4';
|
||||
case 'webm': return 'video/webm';
|
||||
case 'mkv': return 'video/x-matroska';
|
||||
case 'avi': return 'video/x-msvideo';
|
||||
case 'mov': return 'video/quicktime';
|
||||
case 'wmv': return 'video/x-ms-wmv';
|
||||
case 'flv': return 'video/x-flv';
|
||||
default: return 'video/mp4';
|
||||
case "mp4":
|
||||
return "video/mp4";
|
||||
case "webm":
|
||||
return "video/webm";
|
||||
case "mkv":
|
||||
return "video/x-matroska";
|
||||
case "avi":
|
||||
return "video/x-msvideo";
|
||||
case "mov":
|
||||
return "video/quicktime";
|
||||
case "wmv":
|
||||
return "video/x-ms-wmv";
|
||||
case "flv":
|
||||
return "video/x-flv";
|
||||
default:
|
||||
return "video/mp4";
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -836,35 +847,36 @@ export function FileViewer({
|
||||
className="w-full rounded-lg shadow-sm"
|
||||
style={{
|
||||
maxHeight: "calc(100vh - 200px)",
|
||||
backgroundColor: "#000"
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
preload="metadata"
|
||||
onError={(e) => {
|
||||
console.error('Video playback error:', e.currentTarget.error);
|
||||
}}
|
||||
onLoadStart={() => {
|
||||
console.log('Video loading started...');
|
||||
console.error(
|
||||
"Video playback error:",
|
||||
e.currentTarget.error,
|
||||
);
|
||||
}}
|
||||
onLoadedMetadata={(e) => {
|
||||
const video = e.currentTarget;
|
||||
console.log('Video metadata loaded, dimensions:', video.videoWidth, 'x', video.videoHeight);
|
||||
|
||||
// Get video dimensions and notify parent
|
||||
if (onMediaDimensionsChange && video.videoWidth && video.videoHeight) {
|
||||
if (
|
||||
onMediaDimensionsChange &&
|
||||
video.videoWidth &&
|
||||
video.videoHeight
|
||||
) {
|
||||
onMediaDimensionsChange({
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight
|
||||
height: video.videoHeight,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onCanPlay={() => {
|
||||
console.log('Video can start playing');
|
||||
}}
|
||||
>
|
||||
<source src={videoUrl} type={mimeType} />
|
||||
<div className="text-center text-muted-foreground p-4">
|
||||
<AlertCircle className="w-8 h-8 mx-auto mb-2" />
|
||||
<p>Your browser does not support video playback for this format.</p>
|
||||
<p>
|
||||
Your browser does not support video playback for this
|
||||
format.
|
||||
</p>
|
||||
{onDownload && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -884,10 +896,8 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Markdown file editor with live preview */}
|
||||
{fileTypeInfo.type === "markdown" && !showLargeFileWarning && (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Markdown toolbar */}
|
||||
<div className="flex-shrink-0 bg-muted/30 border-b border-border p-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -908,17 +918,13 @@ export function FileViewer({
|
||||
{t("fileManager.preview")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Save button removed - using the main header save button instead */}
|
||||
</div>
|
||||
<div className="flex items-center gap-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Markdown content area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{markdownEditMode ? (
|
||||
<>
|
||||
{/* Editor pane */}
|
||||
<div className="flex-1 border-r border-border">
|
||||
<div className="h-full p-4 bg-background">
|
||||
<textarea
|
||||
@@ -933,14 +939,21 @@ export function FileViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview pane */}
|
||||
<div className="flex-1 overflow-auto bg-muted/10">
|
||||
<div className="p-4">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
code({
|
||||
node,
|
||||
inline,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
const match = /language-(\w+)/.exec(
|
||||
className || "",
|
||||
);
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={syntaxTheme}
|
||||
@@ -949,10 +962,13 @@ export function FileViewer({
|
||||
className="rounded-lg"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className="bg-muted px-1 py-0.5 rounded text-sm font-mono" {...props}>
|
||||
<code
|
||||
className="bg-muted px-1 py-0.5 rounded text-sm font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
@@ -993,9 +1009,7 @@ export function FileViewer({
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 text-foreground">
|
||||
{children}
|
||||
</li>
|
||||
<li className="mb-1 text-foreground">{children}</li>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-blue-500 pl-3 mb-3 italic text-muted-foreground bg-muted/30 py-1">
|
||||
@@ -1010,15 +1024,9 @@ export function FileViewer({
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-muted">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody>
|
||||
{children}
|
||||
</tbody>
|
||||
<thead className="bg-muted">{children}</thead>
|
||||
),
|
||||
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||
tr: ({ children }) => (
|
||||
<tr className="border-b border-border">
|
||||
{children}
|
||||
@@ -1059,7 +1067,7 @@ export function FileViewer({
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const match = /language-(\w+)/.exec(className || "");
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
style={syntaxTheme}
|
||||
@@ -1068,10 +1076,13 @@ export function FileViewer({
|
||||
className="rounded-lg"
|
||||
{...props}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
{String(children).replace(/\n$/, "")}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className="bg-muted px-1 py-0.5 rounded text-sm font-mono" {...props}>
|
||||
<code
|
||||
className="bg-muted px-1 py-0.5 rounded text-sm font-mono"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
@@ -1112,9 +1123,7 @@ export function FileViewer({
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="mb-1 text-foreground">
|
||||
{children}
|
||||
</li>
|
||||
<li className="mb-1 text-foreground">{children}</li>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-blue-500 pl-4 mb-4 italic text-muted-foreground bg-muted/30 py-2">
|
||||
@@ -1129,19 +1138,11 @@ export function FileViewer({
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-muted">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody>
|
||||
{children}
|
||||
</tbody>
|
||||
<thead className="bg-muted">{children}</thead>
|
||||
),
|
||||
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||
tr: ({ children }) => (
|
||||
<tr className="border-b border-border">
|
||||
{children}
|
||||
</tr>
|
||||
<tr className="border-b border-border">{children}</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="px-4 py-2 text-left font-semibold text-foreground">
|
||||
@@ -1174,10 +1175,8 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PDF file preview with react-pdf */}
|
||||
{fileTypeInfo.type === "pdf" && !showLargeFileWarning && (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
{/* PDF Controls */}
|
||||
<div className="flex-shrink-0 bg-muted/30 border-b border-border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -1191,12 +1190,17 @@ export function FileViewer({
|
||||
{t("fileManager.previous")}
|
||||
</Button>
|
||||
<span className="text-sm text-foreground px-3 py-1 bg-background rounded border">
|
||||
{t("fileManager.pageXOfY", { current: pageNumber, total: numPages || 0 })}
|
||||
{t("fileManager.pageXOfY", {
|
||||
current: pageNumber,
|
||||
total: numPages || 0,
|
||||
})}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setPageNumber(Math.min(numPages || 1, pageNumber + 1))}
|
||||
onClick={() =>
|
||||
setPageNumber(Math.min(numPages || 1, pageNumber + 1))
|
||||
}
|
||||
disabled={!numPages || pageNumber >= numPages}
|
||||
>
|
||||
{t("fileManager.next")}
|
||||
@@ -1236,13 +1240,14 @@ export function FileViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PDF Content */}
|
||||
<div className="flex-1 overflow-auto p-6 bg-gray-100 dark:bg-gray-900">
|
||||
<div className="flex justify-center">
|
||||
{pdfError ? (
|
||||
<div className="text-center text-muted-foreground p-8">
|
||||
<AlertCircle className="w-16 h-16 mx-auto mb-4 text-muted-foreground/50" />
|
||||
<h3 className="text-lg font-medium mb-2">Cannot load PDF</h3>
|
||||
<h3 className="text-lg font-medium mb-2">
|
||||
Cannot load PDF
|
||||
</h3>
|
||||
<p className="text-sm mb-4">
|
||||
There was an error loading this PDF file.
|
||||
</p>
|
||||
@@ -1264,22 +1269,23 @@ export function FileViewer({
|
||||
setNumPages(numPages);
|
||||
setPdfError(false);
|
||||
|
||||
// Notify parent about PDF dimensions for window sizing
|
||||
if (onMediaDimensionsChange) {
|
||||
onMediaDimensionsChange({
|
||||
width: 800,
|
||||
height: 600
|
||||
height: 600,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onLoadError={(error) => {
|
||||
console.error('PDF load error:', error);
|
||||
console.error("PDF load error:", error);
|
||||
setPdfError(true);
|
||||
}}
|
||||
loading={
|
||||
<div className="text-center p-8">
|
||||
<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">Loading PDF...</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Loading PDF...
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -1290,7 +1296,9 @@ export function FileViewer({
|
||||
loading={
|
||||
<div className="text-center p-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500 mx-auto mb-2"></div>
|
||||
<p className="text-xs text-muted-foreground">Loading page...</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Loading page...
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@@ -1301,21 +1309,27 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audio file preview with react-h5-audio-player */}
|
||||
{fileTypeInfo.type === "audio" && !showLargeFileWarning && (
|
||||
<div className="p-6 flex items-center justify-center h-full">
|
||||
<div className="w-full max-w-2xl">
|
||||
{(() => {
|
||||
const ext = file.name.split('.').pop()?.toLowerCase() || '';
|
||||
const ext = file.name.split(".").pop()?.toLowerCase() || "";
|
||||
const mimeType = (() => {
|
||||
switch (ext) {
|
||||
case 'mp3': return 'audio/mpeg';
|
||||
case 'wav': return 'audio/wav';
|
||||
case 'flac': return 'audio/flac';
|
||||
case 'ogg': return 'audio/ogg';
|
||||
case 'aac': return 'audio/aac';
|
||||
case 'm4a': return 'audio/mp4';
|
||||
default: return 'audio/mpeg';
|
||||
case "mp3":
|
||||
return "audio/mpeg";
|
||||
case "wav":
|
||||
return "audio/wav";
|
||||
case "flac":
|
||||
return "audio/flac";
|
||||
case "ogg":
|
||||
return "audio/ogg";
|
||||
case "aac":
|
||||
return "audio/aac";
|
||||
case "m4a":
|
||||
return "audio/mp4";
|
||||
default:
|
||||
return "audio/mpeg";
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1323,7 +1337,6 @@ export function FileViewer({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Album artwork placeholder */}
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className={cn(
|
||||
@@ -1335,7 +1348,6 @@ export function FileViewer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Track info */}
|
||||
<div className="text-center">
|
||||
<h3 className="font-semibold text-foreground text-lg mb-1">
|
||||
{file.name.replace(/\.[^/.]+$/, "")}
|
||||
@@ -1345,30 +1357,20 @@ export function FileViewer({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Audio Player */}
|
||||
<div className="rounded-lg overflow-hidden">
|
||||
<AudioPlayer
|
||||
src={audioUrl}
|
||||
onPlay={() => {
|
||||
console.log('Audio playback started');
|
||||
}}
|
||||
onPause={() => {
|
||||
console.log('Audio playback paused');
|
||||
}}
|
||||
onLoadedMetadata={(e) => {
|
||||
const audio = e.currentTarget;
|
||||
console.log('Audio metadata loaded, duration:', audio.duration);
|
||||
|
||||
// Get audio dimensions for window sizing (use a standard audio player height)
|
||||
if (onMediaDimensionsChange) {
|
||||
onMediaDimensionsChange({
|
||||
width: 600,
|
||||
height: 400
|
||||
height: 400,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onError={(e) => {
|
||||
console.error('Audio playback error:', e);
|
||||
console.error("Audio playback error:", e);
|
||||
}}
|
||||
showJumpControls={false}
|
||||
showSkipControls={false}
|
||||
@@ -1384,7 +1386,6 @@ export function FileViewer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Unknown file type - only show when cannot display as text and no warning */}
|
||||
{fileTypeInfo.type === "unknown" &&
|
||||
!shouldShowAsText &&
|
||||
!showLargeFileWarning && (
|
||||
@@ -1413,7 +1414,6 @@ 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>
|
||||
|
||||
@@ -44,8 +44,7 @@ interface FileWindowProps {
|
||||
sshHost: SSHHost;
|
||||
initialX?: number;
|
||||
initialY?: number;
|
||||
onFileNotFound?: (file: FileItem) => void; // Callback for when file is not found
|
||||
// readOnly parameter removed, determined internally by FileViewer based on file type
|
||||
onFileNotFound?: (file: FileItem) => void;
|
||||
}
|
||||
|
||||
export function FileWindow({
|
||||
@@ -57,13 +56,8 @@ export function FileWindow({
|
||||
initialY = 100,
|
||||
onFileNotFound,
|
||||
}: FileWindowProps) {
|
||||
const {
|
||||
closeWindow,
|
||||
maximizeWindow,
|
||||
focusWindow,
|
||||
updateWindow,
|
||||
windows,
|
||||
} = useWindowManager();
|
||||
const { closeWindow, maximizeWindow, focusWindow, updateWindow, windows } =
|
||||
useWindowManager();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -71,22 +65,18 @@ export function FileWindow({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEditable, setIsEditable] = useState(false);
|
||||
const [pendingContent, setPendingContent] = useState<string>("");
|
||||
const [mediaDimensions, setMediaDimensions] = useState<{ width: number; height: number } | undefined>();
|
||||
const [mediaDimensions, setMediaDimensions] = useState<
|
||||
{ width: number; height: number } | undefined
|
||||
>();
|
||||
const autoSaveTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const currentWindow = windows.find((w) => w.id === windowId);
|
||||
|
||||
// Ensure SSH connection is valid
|
||||
const ensureSSHConnection = async () => {
|
||||
try {
|
||||
// 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,17 +89,13 @@ export function FileWindow({
|
||||
credentialId: sshHost.credentialId,
|
||||
userId: sshHost.userId,
|
||||
});
|
||||
|
||||
console.log("SSH reconnection successful");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("SSH connection check/reconnect failed:", error);
|
||||
// Even if connection fails, try to continue and let specific API calls handle errors
|
||||
console.error("SSH connection check/reconnect failed:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Load file content
|
||||
useEffect(() => {
|
||||
const loadFileContent = async () => {
|
||||
if (file.type !== "file") return;
|
||||
@@ -117,23 +103,19 @@ export function FileWindow({
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
const response = await readSSHFile(sshSessionId, file.path);
|
||||
const fileContent = response.content || "";
|
||||
setContent(fileContent);
|
||||
setPendingContent(fileContent); // Initialize pending content
|
||||
setPendingContent(fileContent);
|
||||
|
||||
// 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",
|
||||
@@ -143,7 +125,6 @@ export function FileWindow({
|
||||
"webp",
|
||||
"tiff",
|
||||
"ico",
|
||||
// Audio files
|
||||
"mp3",
|
||||
"wav",
|
||||
"ogg",
|
||||
@@ -151,7 +132,6 @@ export function FileWindow({
|
||||
"flac",
|
||||
"m4a",
|
||||
"wma",
|
||||
// Video files
|
||||
"mp4",
|
||||
"avi",
|
||||
"mov",
|
||||
@@ -160,7 +140,6 @@ export function FileWindow({
|
||||
"mkv",
|
||||
"webm",
|
||||
"m4v",
|
||||
// Archive files
|
||||
"zip",
|
||||
"rar",
|
||||
"7z",
|
||||
@@ -168,7 +147,6 @@ export function FileWindow({
|
||||
"gz",
|
||||
"bz2",
|
||||
"xz",
|
||||
// Binary files
|
||||
"exe",
|
||||
"dll",
|
||||
"so",
|
||||
@@ -178,28 +156,25 @@ 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}`, {
|
||||
duration: 10000, // 10 seconds for important message
|
||||
duration: 10000,
|
||||
});
|
||||
} else if (
|
||||
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})`,
|
||||
);
|
||||
} else {
|
||||
// Check if file not found (common error messages from cat command)
|
||||
const errorMessage = errorData?.error || error.message || "Unknown error";
|
||||
const errorMessage =
|
||||
errorData?.error || error.message || "Unknown error";
|
||||
const isFileNotFound =
|
||||
(error as any).isFileNotFound ||
|
||||
errorData?.fileNotFound ||
|
||||
@@ -211,19 +186,21 @@ export function FileWindow({
|
||||
errorMessage.includes("Resource not found");
|
||||
|
||||
if (isFileNotFound && onFileNotFound) {
|
||||
// Notify parent component about the missing file for cleanup
|
||||
onFileNotFound(file);
|
||||
toast.error(t("fileManager.fileNotFoundAndRemoved", { name: file.name }));
|
||||
toast.error(
|
||||
t("fileManager.fileNotFoundAndRemoved", { name: file.name }),
|
||||
);
|
||||
|
||||
// Close this window since the file doesn't exist
|
||||
closeWindow(windowId);
|
||||
return; // Exit early to prevent showing empty editor
|
||||
return;
|
||||
} else {
|
||||
toast.error(t("fileManager.failedToLoadFile", {
|
||||
error: errorMessage.includes("Server error occurred") ?
|
||||
t("fileManager.serverErrorOccurred") :
|
||||
errorMessage
|
||||
}));
|
||||
toast.error(
|
||||
t("fileManager.failedToLoadFile", {
|
||||
error: errorMessage.includes("Server error occurred")
|
||||
? t("fileManager.serverErrorOccurred")
|
||||
: errorMessage,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -234,19 +211,16 @@ export function FileWindow({
|
||||
loadFileContent();
|
||||
}, [file, sshSessionId, sshHost]);
|
||||
|
||||
// Save file
|
||||
const handleSave = async (newContent: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
await writeSSHFile(sshSessionId, file.path, newContent);
|
||||
setContent(newContent);
|
||||
setPendingContent(""); // Clear pending content
|
||||
setPendingContent("");
|
||||
|
||||
// Clear auto-save timer
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current);
|
||||
autoSaveTimerRef.current = null;
|
||||
@@ -256,7 +230,6 @@ 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")
|
||||
@@ -265,36 +238,33 @@ export function FileWindow({
|
||||
`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`,
|
||||
);
|
||||
} else {
|
||||
toast.error(`${t("fileManager.failedToSaveFile")}: ${error.message || t("fileManager.unknownError")}`);
|
||||
toast.error(
|
||||
`${t("fileManager.failedToSaveFile")}: ${error.message || t("fileManager.unknownError")}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle content changes - set 1-minute auto-save
|
||||
const handleContentChange = (newContent: string) => {
|
||||
setPendingContent(newContent);
|
||||
|
||||
// Clear previous timer
|
||||
if (autoSaveTimerRef.current) {
|
||||
clearTimeout(autoSaveTimerRef.current);
|
||||
}
|
||||
|
||||
// Set new 1-minute auto-save timer
|
||||
autoSaveTimerRef.current = setTimeout(async () => {
|
||||
try {
|
||||
console.log("Auto-saving file...");
|
||||
await handleSave(newContent);
|
||||
toast.success(t("fileManager.fileAutoSaved"));
|
||||
} catch (error) {
|
||||
console.error("Auto-save failed:", error);
|
||||
toast.error(t("fileManager.autoSaveFailed"));
|
||||
}
|
||||
}, 60000); // 1 minute = 60000 milliseconds
|
||||
}, 60000);
|
||||
};
|
||||
|
||||
// Cleanup timer
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoSaveTimerRef.current) {
|
||||
@@ -303,16 +273,13 @@ export function FileWindow({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Download file
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
// Ensure SSH connection is valid
|
||||
await ensureSSHConnection();
|
||||
|
||||
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++) {
|
||||
@@ -337,7 +304,6 @@ 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")
|
||||
@@ -353,7 +319,6 @@ export function FileWindow({
|
||||
}
|
||||
};
|
||||
|
||||
// Window operation handling
|
||||
const handleClose = () => {
|
||||
closeWindow(windowId);
|
||||
};
|
||||
@@ -366,9 +331,10 @@ export function FileWindow({
|
||||
focusWindow(windowId);
|
||||
};
|
||||
|
||||
// Handle media dimensions change
|
||||
const handleMediaDimensionsChange = (dimensions: { width: number; height: number }) => {
|
||||
console.log('Media dimensions received:', dimensions);
|
||||
const handleMediaDimensionsChange = (dimensions: {
|
||||
width: number;
|
||||
height: number;
|
||||
}) => {
|
||||
setMediaDimensions(dimensions);
|
||||
};
|
||||
|
||||
@@ -397,7 +363,7 @@ export function FileWindow({
|
||||
content={pendingContent || content}
|
||||
savedContent={content}
|
||||
isLoading={isLoading}
|
||||
isEditable={isEditable} // Remove forced read-only mode, controlled internally by FileViewer
|
||||
isEditable={isEditable}
|
||||
onContentChange={handleContentChange}
|
||||
onSave={(newContent) => handleSave(newContent)}
|
||||
onDownload={handleDownload}
|
||||
|
||||
@@ -39,10 +39,8 @@ export function TerminalWindow({
|
||||
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } =
|
||||
useWindowManager();
|
||||
|
||||
// Get current window state
|
||||
const currentWindow = windows.find((w) => w.id === windowId);
|
||||
if (!currentWindow) {
|
||||
console.warn(`Window with id ${windowId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -65,7 +63,10 @@ export function TerminalWindow({
|
||||
const terminalTitle = executeCommand
|
||||
? t("terminal.runTitle", { host: hostConfig.name, command: executeCommand })
|
||||
: initialPath
|
||||
? t("terminal.terminalWithPath", { host: hostConfig.name, path: initialPath })
|
||||
? t("terminal.terminalWithPath", {
|
||||
host: hostConfig.name,
|
||||
path: initialPath,
|
||||
})
|
||||
: t("terminal.terminalTitle", { host: hostConfig.name });
|
||||
|
||||
return (
|
||||
|
||||
@@ -35,13 +35,11 @@ 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 +58,10 @@ 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 +70,6 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Maximize/restore window
|
||||
const maximizeWindow = useCallback((id: string) => {
|
||||
setWindows((prev) =>
|
||||
prev.map((w) =>
|
||||
@@ -83,7 +78,6 @@ 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 +88,6 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Update window properties
|
||||
const updateWindow = useCallback(
|
||||
(id: string, updates: Partial<WindowInstance>) => {
|
||||
setWindows((prev) =>
|
||||
@@ -117,7 +110,6 @@ 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}>
|
||||
@@ -131,7 +123,6 @@ export function WindowManager({ children }: WindowManagerProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Hook for using window manager
|
||||
export function useWindowManager() {
|
||||
const context = React.useContext(WindowManagerContext);
|
||||
if (!context) {
|
||||
|
||||
Reference in New Issue
Block a user