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

@@ -17,6 +17,7 @@ import { SimpleDBOps } from "../../utils/simple-db-ops.js";
import { AuthManager } from "../../utils/auth-manager.js"; import { AuthManager } from "../../utils/auth-manager.js";
import { DataCrypto } from "../../utils/data-crypto.js"; import { DataCrypto } from "../../utils/data-crypto.js";
import { SystemCrypto } from "../../utils/system-crypto.js"; import { SystemCrypto } from "../../utils/system-crypto.js";
import { DatabaseSaveTrigger } from "../db/index.js";
const router = express.Router(); const router = express.Router();
@@ -1156,6 +1157,9 @@ router.put(
) )
.returning(); .returning();
// Trigger database save after folder rename
DatabaseSaveTrigger.triggerSave("folder_rename");
res.json({ res.json({
message: "Folder renamed successfully", message: "Folder renamed successfully",
updatedHosts: updatedHosts.length, updatedHosts: updatedHosts.length,

View File

@@ -0,0 +1,162 @@
import { databaseLogger } from "./logger.js";
/**
* Database Save Trigger - 自动触发内存数据库保存到磁盘
* 确保数据修改后能持久化保存
*/
export class DatabaseSaveTrigger {
private static saveFunction: (() => Promise<void>) | null = null;
private static isInitialized = false;
private static pendingSave = false;
private static saveTimeout: NodeJS.Timeout | null = null;
/**
* 初始化保存触发器
*/
static initialize(saveFunction: () => Promise<void>): void {
this.saveFunction = saveFunction;
this.isInitialized = true;
databaseLogger.info("Database save trigger initialized", {
operation: "db_save_trigger_init",
});
}
/**
* 触发数据库保存 - 防抖处理,避免频繁保存
*/
static async triggerSave(reason: string = "data_modification"): Promise<void> {
if (!this.isInitialized || !this.saveFunction) {
databaseLogger.warn("Database save trigger not initialized", {
operation: "db_save_trigger_not_init",
reason,
});
return;
}
// 清除之前的定时器
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
// 防抖延迟2秒执行如果2秒内有新的保存请求则重新计时
this.saveTimeout = setTimeout(async () => {
if (this.pendingSave) {
databaseLogger.debug("Database save already in progress, skipping", {
operation: "db_save_trigger_skip",
reason,
});
return;
}
this.pendingSave = true;
try {
databaseLogger.debug("Triggering database save", {
operation: "db_save_trigger_start",
reason,
});
await this.saveFunction!();
databaseLogger.debug("Database save completed", {
operation: "db_save_trigger_success",
reason,
});
} catch (error) {
databaseLogger.error("Database save failed", error, {
operation: "db_save_trigger_failed",
reason,
error: error instanceof Error ? error.message : "Unknown error",
});
} finally {
this.pendingSave = false;
}
}, 2000); // 2秒防抖
}
/**
* 立即保存 - 用于关键操作
*/
static async forceSave(reason: string = "critical_operation"): Promise<void> {
if (!this.isInitialized || !this.saveFunction) {
databaseLogger.warn("Database save trigger not initialized for force save", {
operation: "db_save_trigger_force_not_init",
reason,
});
return;
}
// 清除防抖定时器
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
this.saveTimeout = null;
}
if (this.pendingSave) {
databaseLogger.debug("Database save already in progress, waiting", {
operation: "db_save_trigger_force_wait",
reason,
});
return;
}
this.pendingSave = true;
try {
databaseLogger.info("Force saving database", {
operation: "db_save_trigger_force_start",
reason,
});
await this.saveFunction();
databaseLogger.success("Database force save completed", {
operation: "db_save_trigger_force_success",
reason,
});
} catch (error) {
databaseLogger.error("Database force save failed", error, {
operation: "db_save_trigger_force_failed",
reason,
error: error instanceof Error ? error.message : "Unknown error",
});
throw error; // 重新抛出错误,因为这是强制保存
} finally {
this.pendingSave = false;
}
}
/**
* 获取保存状态
*/
static getStatus(): {
initialized: boolean;
pendingSave: boolean;
hasPendingTimeout: boolean;
} {
return {
initialized: this.isInitialized,
pendingSave: this.pendingSave,
hasPendingTimeout: this.saveTimeout !== null,
};
}
/**
* 清理资源
*/
static cleanup(): void {
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
this.saveTimeout = null;
}
this.pendingSave = false;
this.isInitialized = false;
this.saveFunction = null;
databaseLogger.info("Database save trigger cleaned up", {
operation: "db_save_trigger_cleanup",
});
}
}

View File

@@ -1,4 +1,4 @@
import { getDb } from "../database/db/index.js"; import { getDb, DatabaseSaveTrigger } from "../database/db/index.js";
import { DataCrypto } from "./data-crypto.js"; import { DataCrypto } from "./data-crypto.js";
import { databaseLogger } from "./logger.js"; import { databaseLogger } from "./logger.js";
import type { SQLiteTable } from "drizzle-orm/sqlite-core"; import type { SQLiteTable } from "drizzle-orm/sqlite-core";
@@ -42,6 +42,9 @@ class SimpleDBOps {
// Insert into database // Insert into database
const result = await getDb().insert(table).values(encryptedData).returning(); const result = await getDb().insert(table).values(encryptedData).returning();
// Trigger database save after insert
DatabaseSaveTrigger.triggerSave(`insert_${tableName}`);
// Decrypt return result using the same key - FieldCrypto will use stored recordId // Decrypt return result using the same key - FieldCrypto will use stored recordId
const decryptedResult = DataCrypto.decryptRecord( const decryptedResult = DataCrypto.decryptRecord(
tableName, tableName,
@@ -170,6 +173,9 @@ class SimpleDBOps {
): Promise<any[]> { ): Promise<any[]> {
const result = await getDb().delete(table).where(where).returning(); const result = await getDb().delete(table).where(where).returning();
// Trigger database save after delete
DatabaseSaveTrigger.triggerSave(`delete_${tableName}`);
return result; return result;
} }

View File

@@ -651,7 +651,10 @@
"reconnecting": "Reconnecting... ({{attempt}}/{{max}})", "reconnecting": "Reconnecting... ({{attempt}}/{{max}})",
"reconnected": "Reconnected successfully", "reconnected": "Reconnected successfully",
"maxReconnectAttemptsReached": "Maximum reconnection attempts reached", "maxReconnectAttemptsReached": "Maximum reconnection attempts reached",
"connectionTimeout": "Connection timeout" "connectionTimeout": "Connection timeout",
"terminalTitle": "Terminal - {{host}}",
"terminalWithPath": "Terminal - {{host}}:{{path}}",
"runTitle": "Running {{command}} - {{host}}"
}, },
"fileManager": { "fileManager": {
"title": "File Manager", "title": "File Manager",
@@ -659,7 +662,7 @@
"folder": "Folder", "folder": "Folder",
"connectToSsh": "Connect to SSH to use file operations", "connectToSsh": "Connect to SSH to use file operations",
"uploadFile": "Upload File", "uploadFile": "Upload File",
"downloadFile": "Download to Browser", "downloadFile": "Download",
"newFile": "New File", "newFile": "New File",
"newFolder": "New Folder", "newFolder": "New Folder",
"rename": "Rename", "rename": "Rename",

View File

@@ -639,6 +639,9 @@
}, },
"terminal": { "terminal": {
"title": "终端", "title": "终端",
"terminalTitle": "终端 - {{host}}",
"terminalWithPath": "终端 - {{host}}:{{path}}",
"runTitle": "运行 {{command}} - {{host}}",
"connect": "连接主机", "connect": "连接主机",
"disconnect": "断开连接", "disconnect": "断开连接",
"clear": "清屏", "clear": "清屏",
@@ -674,7 +677,7 @@
"folder": "文件夹", "folder": "文件夹",
"connectToSsh": "连接 SSH 以使用文件操作", "connectToSsh": "连接 SSH 以使用文件操作",
"uploadFile": "上传文件", "uploadFile": "上传文件",
"downloadFile": "下载到浏览器", "downloadFile": "下载",
"newFile": "新建文件", "newFile": "新建文件",
"newFolder": "新建文件夹", "newFolder": "新建文件夹",
"rename": "重命名", "rename": "重命名",
@@ -741,7 +744,7 @@
"properties": "属性", "properties": "属性",
"preview": "预览", "preview": "预览",
"refresh": "刷新", "refresh": "刷新",
"downloadFiles": "下载 {{count}} 个文件到浏览器", "downloadFiles": "下载 {{count}} 个文件",
"copyFiles": "复制 {{count}} 个项目", "copyFiles": "复制 {{count}} 个项目",
"cutFiles": "剪切 {{count}} 个项目", "cutFiles": "剪切 {{count}} 个项目",
"deleteFiles": "删除 {{count}} 个项目", "deleteFiles": "删除 {{count}} 个项目",

View File

@@ -225,7 +225,7 @@ export function FileManagerContextMenu({
? t("fileManager.openTerminalInFolder") ? t("fileManager.openTerminalInFolder")
: t("fileManager.openTerminalInFileLocation"), : t("fileManager.openTerminalInFileLocation"),
action: () => onOpenTerminal(targetPath), action: () => onOpenTerminal(targetPath),
shortcut: "Ctrl+T", shortcut: "Ctrl+Shift+T",
}); });
} }
@@ -257,30 +257,15 @@ export function FileManagerContextMenu({
}); });
} }
// Download function // Download function - unified download that uses best available method
if (hasFiles && onDownload) { if (hasFiles && onDragToDesktop) {
menuItems.push({ menuItems.push({
icon: <Download className="w-4 h-4" />, icon: <Download className="w-4 h-4" />,
label: isMultipleFiles label: isMultipleFiles
? t("fileManager.downloadFiles", { count: files.length }) ? t("fileManager.downloadFiles", { count: files.length })
: t("fileManager.downloadFile"), : 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(), action: () => onDragToDesktop(),
shortcut: isModernBrowser shortcut: "Ctrl+D",
? t("fileManager.selectLocationToSave")
: t("fileManager.downloadToDefaultLocation"),
}); });
} }
@@ -314,7 +299,7 @@ export function FileManagerContextMenu({
// Add separator (if above functions exist) // Add separator (if above functions exist)
if ( if (
(hasFiles && (onPreview || onDownload || onDragToDesktop)) || (hasFiles && (onPreview || onDragToDesktop)) ||
(isSingleFile && (isSingleFile &&
files[0].type === "file" && files[0].type === "file" &&
(onPinFile || onUnpinFile)) || (onPinFile || onUnpinFile)) ||
@@ -329,7 +314,7 @@ export function FileManagerContextMenu({
icon: <Edit3 className="w-4 h-4" />, icon: <Edit3 className="w-4 h-4" />,
label: t("fileManager.rename"), label: t("fileManager.rename"),
action: () => onRename(files[0]), action: () => onRename(files[0]),
shortcut: "F2", shortcut: "F6",
}); });
} }
@@ -397,7 +382,7 @@ export function FileManagerContextMenu({
icon: <Terminal className="w-4 h-4" />, icon: <Terminal className="w-4 h-4" />,
label: t("fileManager.openTerminalHere"), label: t("fileManager.openTerminalHere"),
action: () => onOpenTerminal(currentPath), action: () => onOpenTerminal(currentPath),
shortcut: "Ctrl+T", shortcut: "Ctrl+Shift+T",
}); });
} }
@@ -447,7 +432,7 @@ export function FileManagerContextMenu({
icon: <RefreshCw className="w-4 h-4" />, icon: <RefreshCw className="w-4 h-4" />,
label: t("fileManager.refresh"), label: t("fileManager.refresh"),
action: onRefresh, action: onRefresh,
shortcut: "F5", shortcut: "Ctrl+Y",
}); });
} }
@@ -487,12 +472,12 @@ export function FileManagerContextMenu({
return ( return (
<> <>
{/* Transparent overlay to capture click events */} {/* Transparent overlay to capture click events */}
<div className="fixed inset-0 z-40" /> <div className="fixed inset-0 z-[99990]" />
{/* Menu body */} {/* Menu body */}
<div <div
data-context-menu 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={{ style={{
left: menuPosition.x, left: menuPosition.x,
top: menuPosition.y, top: menuPosition.y,

View File

@@ -744,13 +744,10 @@ export function FileManagerGrid({
e.stopPropagation(); e.stopPropagation();
if (dragState.type === "internal") { if (dragState.type === "internal") {
// Internal drag to empty area: trigger download // Internal drag to empty area: just cancel the drag operation
console.log( console.log("Internal drag to empty area - cancelling drag operation");
"Internal drag to empty area detected, triggering download", // Do not trigger download here - system drag end will handle it if truly outside window
); setDragState({ type: "none", files: [], counter: 0 });
if (onDownload && dragState.files.length > 0) {
onDownload(dragState.files);
}
} else if (dragState.type === "external") { } else if (dragState.type === "external") {
// External drag: handle file upload // External drag: handle file upload
if (onUpload && e.dataTransfer.files.length > 0) { if (onUpload && e.dataTransfer.files.length > 0) {
@@ -914,15 +911,18 @@ export function FileManagerGrid({
onDelete(selectedFiles); onDelete(selectedFiles);
} }
break; break;
case "F2": case "F6":
if (selectedFiles.length === 1 && onStartEdit) { if (selectedFiles.length === 1 && onStartEdit) {
event.preventDefault(); event.preventDefault();
onStartEdit(selectedFiles[0]); onStartEdit(selectedFiles[0]);
} }
break; break;
case "F5": case "y":
event.preventDefault(); case "Y":
onRefresh(); if ((event.ctrlKey || event.metaKey)) {
event.preventDefault();
onRefresh();
}
break; break;
} }
}; };
@@ -1148,7 +1148,7 @@ export function FileManagerGrid({
"hover:bg-accent hover:text-accent-foreground border-2 border-transparent", "hover:bg-accent hover:text-accent-foreground border-2 border-transparent",
isSelected && "bg-primary/20 border-primary", isSelected && "bg-primary/20 border-primary",
dragState.target?.path === file.path && 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) && dragState.files.some((f) => f.path === file.path) &&
"opacity-50", "opacity-50",
)} )}
@@ -1188,15 +1188,8 @@ export function FileManagerGrid({
/> />
) : ( ) : (
<p <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" className="text-xs text-foreground truncate px-1 py-0.5 rounded w-fit max-w-full text-center"
title={`${file.name} (click to rename)`} title={file.name}
onClick={(e) => {
// Prevent file selection event
if (onStartEdit) {
e.stopPropagation();
onStartEdit(file);
}
}}
> >
{file.name} {file.name}
</p> </p>
@@ -1248,7 +1241,7 @@ export function FileManagerGrid({
"hover:bg-accent hover:text-accent-foreground", "hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-primary/20", isSelected && "bg-primary/20",
dragState.target?.path === file.path && 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) && dragState.files.some((f) => f.path === file.path) &&
"opacity-50", "opacity-50",
)} )}
@@ -1288,15 +1281,8 @@ export function FileManagerGrid({
/> />
) : ( ) : (
<p <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" className="text-sm text-foreground truncate px-1 py-0.5 rounded w-fit max-w-full"
title={`${file.name} (click to rename)`} title={file.name}
onClick={(e) => {
// Prevent file selection event
if (onStartEdit) {
e.stopPropagation();
onStartEdit(file);
}
}}
> >
{file.name} {file.name}
</p> </p>
@@ -1373,10 +1359,10 @@ export function FileManagerGrid({
dragState.files.length > 0 && dragState.files.length > 0 &&
dragState.mousePosition && ( dragState.mousePosition && (
<div <div
className="fixed z-50 pointer-events-none" className="fixed z-[99999] pointer-events-none"
style={{ style={{
left: dragState.mousePosition.x + 16, left: dragState.mousePosition.x + 24,
top: dragState.mousePosition.y - 8, 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"> <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 // SSH keepalive timer
const keepaliveTimerRef = useRef<NodeJS.Timeout | null>(null); 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 // Handle file drag to external
const handleFileDragStart = useCallback( const handleFileDragStart = useCallback(
(files: FileItem[]) => { (files: FileItem[]) => {
@@ -273,10 +234,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
e.clientY > window.innerHeight - margin; e.clientY > window.innerHeight - margin;
if (isOutside) { if (isOutside) {
// Delay execution to avoid conflicts with other events // Execute immediately to preserve user gesture context
setTimeout(() => { systemDrag.handleDragEnd(e);
systemDrag.handleDragEnd(e);
}, 100);
} else { } else {
// Cancel drag // Cancel drag
systemDrag.cancelDragToSystem(); systemDrag.cancelDragToSystem();
@@ -386,6 +345,45 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
} }
}, [sshSessionId, isLoading, clearSelection, t]); }, [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 // Debounced refresh function - prevent excessive clicking
const handleRefreshDirectory = useCallback(() => { const handleRefreshDirectory = useCallback(() => {
const now = Date.now(); const now = Date.now();
@@ -400,6 +398,31 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
loadDirectory(currentPath); loadDirectory(currentPath);
}, [currentPath, lastRefreshTime, loadDirectory]); }, [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) { function handleFilesDropped(fileList: FileList) {
if (!sshSessionId) { if (!sshSessionId) {
toast.error(t("fileManager.noSSHConnection")); toast.error(t("fileManager.noSSHConnection"));
@@ -1382,7 +1405,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
}); });
toast.success( 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, initialY = 100,
}: DiffWindowProps) { }: DiffWindowProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { closeWindow, minimizeWindow, maximizeWindow, focusWindow, windows } = const { closeWindow, maximizeWindow, focusWindow, windows } =
useWindowManager(); useWindowManager();
const currentWindow = windows.find((w) => w.id === windowId); const currentWindow = windows.find((w) => w.id === windowId);
@@ -35,10 +35,6 @@ export function DiffWindow({
closeWindow(windowId); closeWindow(windowId);
}; };
const handleMinimize = () => {
minimizeWindow(windowId);
};
const handleMaximize = () => { const handleMaximize = () => {
maximizeWindow(windowId); maximizeWindow(windowId);
}; };
@@ -61,7 +57,6 @@ export function DiffWindow({
minWidth={800} minWidth={800}
minHeight={500} minHeight={500}
onClose={handleClose} onClose={handleClose}
onMinimize={handleMinimize}
onMaximize={handleMaximize} onMaximize={handleMaximize}
onFocus={handleFocus} onFocus={handleFocus}
isMaximized={currentWindow.isMaximized} isMaximized={currentWindow.isMaximized}

View File

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

View File

@@ -130,40 +130,6 @@ export function useDragToSystemDesktop({
return await zip.generateAsync({ type: "blob" }); return await zip.generateAsync({ type: "blob" });
}; };
// Save file using File System Access API
const saveFileWithSystemAPI = async (blob: Blob, suggestedName: string) => {
try {
// Get last saved directory handle
const lastDirHandle = await getLastSaveDirectory();
const fileHandle = await (window as any).showSaveFilePicker({
suggestedName,
startIn: lastDirHandle || "desktop", // Prefer last directory, otherwise desktop
types: [
{
description: "Files",
accept: {
"*/*": [".txt", ".jpg", ".png", ".pdf", ".zip", ".tar", ".gz"],
},
},
],
});
// Save current directory handle for next use
await saveLastDirectory(fileHandle);
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();
return true;
} catch (error: any) {
if (error.name === "AbortError") {
return false; // User cancelled
}
throw error;
}
};
// Fallback solution: traditional download // Fallback solution: traditional download
const fallbackDownload = (blob: Blob, fileName: string) => { const fallbackDownload = (blob: Blob, fileName: string) => {
@@ -206,35 +172,62 @@ export function useDragToSystemDesktop({
error: null, error: null,
})); }));
let blob: Blob; // Determine file name first (synchronously)
let fileName: string; const fileName = fileList.length === 1
? fileList[0].name
: `files_${Date.now()}.zip`;
// For File System Access API, get the file handle FIRST to preserve user gesture
let fileHandle: any = null;
if (isFileSystemAPISupported()) {
try {
fileHandle = await (window as any).showSaveFilePicker({
suggestedName: fileName,
startIn: "desktop",
types: [
{
description: "Files",
accept: {
"*/*": [".txt", ".jpg", ".png", ".pdf", ".zip", ".tar", ".gz"],
},
},
],
});
} catch (error: any) {
if (error.name === "AbortError") {
// User cancelled
setState((prev) => ({
...prev,
isDownloading: false,
progress: 0,
}));
return false;
}
throw error;
}
}
// Now create the blob (after getting file handle)
let blob: Blob;
if (fileList.length === 1) { if (fileList.length === 1) {
// Single file // Single file
blob = await createFileBlob(fileList[0]); blob = await createFileBlob(fileList[0]);
fileName = fileList[0].name;
setState((prev) => ({ ...prev, progress: 70 })); setState((prev) => ({ ...prev, progress: 70 }));
} else { } else {
// Package multiple files into ZIP // Package multiple files into ZIP
blob = await createZipBlob(fileList); blob = await createZipBlob(fileList);
fileName = `files_${Date.now()}.zip`;
setState((prev) => ({ ...prev, progress: 70 })); setState((prev) => ({ ...prev, progress: 70 }));
} }
setState((prev) => ({ ...prev, progress: 90 })); setState((prev) => ({ ...prev, progress: 90 }));
// Prefer File System Access API // Save the file
if (isFileSystemAPISupported()) { if (fileHandle) {
const saved = await saveFileWithSystemAPI(blob, fileName); // Use File System Access API with pre-obtained handle
if (!saved) { await saveLastDirectory(fileHandle);
// User cancelled const writable = await fileHandle.createWritable();
setState((prev) => ({ await writable.write(blob);
...prev, await writable.close();
isDownloading: false,
progress: 0,
}));
return false;
}
} else { } else {
// Fallback to traditional download // Fallback to traditional download
fallbackDownload(blob, fileName); fallbackDownload(blob, fileName);
@@ -301,10 +294,8 @@ export function useDragToSystemDesktop({
// Check if dragged outside window // Check if dragged outside window
if (isDraggedOutsideWindow(e)) { if (isDraggedOutsideWindow(e)) {
// Delayed execution to avoid conflicts with other drag events // Execute immediately to preserve user gesture context for showSaveFilePicker
setTimeout(() => { handleDragToSystem(files, options);
handleDragToSystem(files, options);
}, 100);
} }
// Clean up drag state // Clean up drag state