FIX: Comprehensive file manager UI/UX improvements and bug fixes

- Fix missing i18n for terminal.terminalWithPath translation key
- Update keyboard shortcuts: remove Ctrl+T conflicts, change refresh to Ctrl+Y, rename shortcut to F6
- Remove click-to-rename functionality to prevent accidental renaming
- Fix drag preview z-index and positioning issues during file operations
- Remove false download trigger when dragging files to original position
- Fix 'Must be handling a user gesture' error in drag-to-desktop functionality
- Remove useless minimize button from file editor and diff viewer windows
- Improve context menu z-index hierarchy for better layering
- Add comprehensive drag state management and visual feedback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZcR
2025-09-24 05:38:30 +08:00
parent 5f5397b924
commit ece6ec0892
11 changed files with 326 additions and 174 deletions

View File

@@ -225,7 +225,7 @@ export function FileManagerContextMenu({
? t("fileManager.openTerminalInFolder")
: t("fileManager.openTerminalInFileLocation"),
action: () => onOpenTerminal(targetPath),
shortcut: "Ctrl+T",
shortcut: "Ctrl+Shift+T",
});
}
@@ -257,30 +257,15 @@ export function FileManagerContextMenu({
});
}
// Download function
if (hasFiles && onDownload) {
// Download function - unified download that uses best available method
if (hasFiles && onDragToDesktop) {
menuItems.push({
icon: <Download className="w-4 h-4" />,
label: isMultipleFiles
? t("fileManager.downloadFiles", { count: files.length })
: t("fileManager.downloadFile"),
action: () => onDownload(files),
shortcut: "Ctrl+D",
});
}
// Drag to desktop menu item (supports browser and desktop apps)
if (hasFiles && onDragToDesktop) {
const isModernBrowser = "showSaveFilePicker" in window;
menuItems.push({
icon: <ExternalLink className="w-4 h-4" />,
label: isMultipleFiles
? t("fileManager.saveFilesToSystem", { count: files.length })
: t("fileManager.saveToSystem"),
action: () => onDragToDesktop(),
shortcut: isModernBrowser
? t("fileManager.selectLocationToSave")
: t("fileManager.downloadToDefaultLocation"),
shortcut: "Ctrl+D",
});
}
@@ -314,7 +299,7 @@ export function FileManagerContextMenu({
// Add separator (if above functions exist)
if (
(hasFiles && (onPreview || onDownload || onDragToDesktop)) ||
(hasFiles && (onPreview || onDragToDesktop)) ||
(isSingleFile &&
files[0].type === "file" &&
(onPinFile || onUnpinFile)) ||
@@ -329,7 +314,7 @@ export function FileManagerContextMenu({
icon: <Edit3 className="w-4 h-4" />,
label: t("fileManager.rename"),
action: () => onRename(files[0]),
shortcut: "F2",
shortcut: "F6",
});
}
@@ -397,7 +382,7 @@ export function FileManagerContextMenu({
icon: <Terminal className="w-4 h-4" />,
label: t("fileManager.openTerminalHere"),
action: () => onOpenTerminal(currentPath),
shortcut: "Ctrl+T",
shortcut: "Ctrl+Shift+T",
});
}
@@ -447,7 +432,7 @@ export function FileManagerContextMenu({
icon: <RefreshCw className="w-4 h-4" />,
label: t("fileManager.refresh"),
action: onRefresh,
shortcut: "F5",
shortcut: "Ctrl+Y",
});
}
@@ -487,12 +472,12 @@ export function FileManagerContextMenu({
return (
<>
{/* Transparent overlay to capture click events */}
<div className="fixed inset-0 z-40" />
<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-50 overflow-hidden"
className="fixed bg-dark-bg border border-dark-border rounded-lg shadow-xl min-w-[180px] max-w-[250px] z-[99995] overflow-hidden"
style={{
left: menuPosition.x,
top: menuPosition.y,

View File

@@ -744,13 +744,10 @@ export function FileManagerGrid({
e.stopPropagation();
if (dragState.type === "internal") {
// Internal drag to empty area: trigger download
console.log(
"Internal drag to empty area detected, triggering download",
);
if (onDownload && dragState.files.length > 0) {
onDownload(dragState.files);
}
// 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) {
@@ -914,15 +911,18 @@ export function FileManagerGrid({
onDelete(selectedFiles);
}
break;
case "F2":
case "F6":
if (selectedFiles.length === 1 && onStartEdit) {
event.preventDefault();
onStartEdit(selectedFiles[0]);
}
break;
case "F5":
event.preventDefault();
onRefresh();
case "y":
case "Y":
if ((event.ctrlKey || event.metaKey)) {
event.preventDefault();
onRefresh();
}
break;
}
};
@@ -1148,7 +1148,7 @@ export function FileManagerGrid({
"hover:bg-accent hover:text-accent-foreground border-2 border-transparent",
isSelected && "bg-primary/20 border-primary",
dragState.target?.path === file.path &&
"bg-muted border-primary border-dashed",
"bg-muted border-primary border-dashed relative z-10",
dragState.files.some((f) => f.path === file.path) &&
"opacity-50",
)}
@@ -1188,15 +1188,8 @@ export function FileManagerGrid({
/>
) : (
<p
className="text-xs text-foreground truncate cursor-pointer hover:bg-accent px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full text-center"
title={`${file.name} (click to rename)`}
onClick={(e) => {
// Prevent file selection event
if (onStartEdit) {
e.stopPropagation();
onStartEdit(file);
}
}}
className="text-xs text-foreground truncate px-1 py-0.5 rounded w-fit max-w-full text-center"
title={file.name}
>
{file.name}
</p>
@@ -1248,7 +1241,7 @@ export function FileManagerGrid({
"hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-primary/20",
dragState.target?.path === file.path &&
"bg-muted border-primary border-dashed",
"bg-muted border-primary border-dashed relative z-10",
dragState.files.some((f) => f.path === file.path) &&
"opacity-50",
)}
@@ -1288,15 +1281,8 @@ export function FileManagerGrid({
/>
) : (
<p
className="text-sm text-foreground truncate cursor-pointer hover:bg-accent px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full"
title={`${file.name} (click to rename)`}
onClick={(e) => {
// Prevent file selection event
if (onStartEdit) {
e.stopPropagation();
onStartEdit(file);
}
}}
className="text-sm text-foreground truncate px-1 py-0.5 rounded w-fit max-w-full"
title={file.name}
>
{file.name}
</p>
@@ -1373,10 +1359,10 @@ export function FileManagerGrid({
dragState.files.length > 0 &&
dragState.mousePosition && (
<div
className="fixed z-50 pointer-events-none"
className="fixed z-[99999] pointer-events-none"
style={{
left: dragState.mousePosition.x + 16,
top: dragState.mousePosition.y - 8,
left: dragState.mousePosition.x + 24,
top: dragState.mousePosition.y - 40,
}}
>
<div className="bg-background border border-border rounded-md shadow-md px-3 py-2 flex items-center gap-2">

View File

@@ -206,45 +206,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
// SSH keepalive timer
const keepaliveTimerRef = useRef<NodeJS.Timeout | null>(null);
// Debounced directory loading for path changes
const debouncedLoadDirectory = useCallback((path: string) => {
// Clear any existing timer
if (pathChangeTimerRef.current) {
clearTimeout(pathChangeTimerRef.current);
}
// Set new timer for debounced loading
pathChangeTimerRef.current = setTimeout(() => {
if (path !== lastPathChangeRef.current && sshSessionId) {
console.log("Loading directory after path change:", path);
lastPathChangeRef.current = path;
loadDirectory(path);
}
}, 150); // 150ms debounce for path changes
}, [sshSessionId, loadDirectory]);
// File list update - only reload when path changes, not on initial connection
useEffect(() => {
if (sshSessionId && currentPath) {
// Skip the first load since it's handled in initializeSSHConnection
if (!initialLoadDoneRef.current) {
initialLoadDoneRef.current = true;
lastPathChangeRef.current = currentPath;
return;
}
// Use debounced loading for path changes to prevent rapid clicking issues
debouncedLoadDirectory(currentPath);
}
// Cleanup timer on unmount or dependency change
return () => {
if (pathChangeTimerRef.current) {
clearTimeout(pathChangeTimerRef.current);
}
};
}, [sshSessionId, currentPath, debouncedLoadDirectory]);
// Handle file drag to external
const handleFileDragStart = useCallback(
(files: FileItem[]) => {
@@ -273,10 +234,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
e.clientY > window.innerHeight - margin;
if (isOutside) {
// Delay execution to avoid conflicts with other events
setTimeout(() => {
systemDrag.handleDragEnd(e);
}, 100);
// Execute immediately to preserve user gesture context
systemDrag.handleDragEnd(e);
} else {
// Cancel drag
systemDrag.cancelDragToSystem();
@@ -386,6 +345,45 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
}
}, [sshSessionId, isLoading, clearSelection, t]);
// Debounced directory loading for path changes
const debouncedLoadDirectory = useCallback((path: string) => {
// Clear any existing timer
if (pathChangeTimerRef.current) {
clearTimeout(pathChangeTimerRef.current);
}
// Set new timer for debounced loading
pathChangeTimerRef.current = setTimeout(() => {
if (path !== lastPathChangeRef.current && sshSessionId) {
console.log("Loading directory after path change:", path);
lastPathChangeRef.current = path;
loadDirectory(path);
}
}, 150); // 150ms debounce for path changes
}, [sshSessionId, loadDirectory]);
// File list update - only reload when path changes, not on initial connection
useEffect(() => {
if (sshSessionId && currentPath) {
// Skip the first load since it's handled in initializeSSHConnection
if (!initialLoadDoneRef.current) {
initialLoadDoneRef.current = true;
lastPathChangeRef.current = currentPath;
return;
}
// Use debounced loading for path changes to prevent rapid clicking issues
debouncedLoadDirectory(currentPath);
}
// Cleanup timer on unmount or dependency change
return () => {
if (pathChangeTimerRef.current) {
clearTimeout(pathChangeTimerRef.current);
}
};
}, [sshSessionId, currentPath, debouncedLoadDirectory]);
// Debounced refresh function - prevent excessive clicking
const handleRefreshDirectory = useCallback(() => {
const now = Date.now();
@@ -400,6 +398,31 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
loadDirectory(currentPath);
}, [currentPath, lastRefreshTime, loadDirectory]);
// Global keyboard shortcuts
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Check if input box or editable element has focus, skip if so
const activeElement = document.activeElement;
if (
activeElement &&
(activeElement.tagName === "INPUT" ||
activeElement.tagName === "TEXTAREA" ||
activeElement.contentEditable === "true")
) {
return;
}
// Handle Ctrl+Shift+T for opening terminal
if (event.key === "T" && event.ctrlKey && event.shiftKey) {
event.preventDefault();
handleOpenTerminal(currentPath);
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [currentPath]);
function handleFilesDropped(fileList: FileList) {
if (!sshSessionId) {
toast.error(t("fileManager.noSSHConnection"));
@@ -1382,7 +1405,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
});
toast.success(
t("fileManager.terminalWithPath", { host: currentHost.name, path }),
t("terminal.terminalWithPath", { host: currentHost.name, path }),
);
}

View File

@@ -25,7 +25,7 @@ export function DiffWindow({
initialY = 100,
}: DiffWindowProps) {
const { t } = useTranslation();
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } =
const { closeWindow, maximizeWindow, focusWindow, windows } =
useWindowManager();
const currentWindow = windows.find((w) => w.id === windowId);
@@ -35,10 +35,6 @@ export function DiffWindow({
closeWindow(windowId);
};
const handleMinimize = () => {
minimizeWindow(windowId);
};
const handleMaximize = () => {
maximizeWindow(windowId);
};
@@ -61,7 +57,6 @@ export function DiffWindow({
minWidth={800}
minHeight={500}
onClose={handleClose}
onMinimize={handleMinimize}
onMaximize={handleMaximize}
onFocus={handleFocus}
isMaximized={currentWindow.isMaximized}

View File

@@ -56,7 +56,6 @@ export function FileWindow({
}: FileWindowProps) {
const {
closeWindow,
minimizeWindow,
maximizeWindow,
focusWindow,
updateWindow,
@@ -329,10 +328,6 @@ export function FileWindow({
closeWindow(windowId);
};
const handleMinimize = () => {
minimizeWindow(windowId);
};
const handleMaximize = () => {
maximizeWindow(windowId);
};
@@ -355,7 +350,6 @@ export function FileWindow({
minWidth={400}
minHeight={300}
onClose={handleClose}
onMinimize={handleMinimize}
onMaximize={handleMaximize}
onFocus={handleFocus}
isMaximized={currentWindow.isMaximized}