import React, { useState, useEffect } from "react"; import { cn } from "@/lib/utils"; import { ChevronRight, ChevronDown, Folder, File, Star, Clock, Bookmark, FolderOpen, } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { SSHHost } from "@/types/index"; import { getRecentFiles, getPinnedFiles, getFolderShortcuts, listSSHFiles, removeRecentFile, removePinnedFile, removeFolderShortcut, } from "@/ui/main-axios.ts"; import { toast } from "sonner"; interface RecentFileData { id: number; name: string; path: string; lastOpened?: string; [key: string]: unknown; } interface PinnedFileData { id: number; name: string; path: string; [key: string]: unknown; } interface ShortcutData { id: number; name: string; path: string; [key: string]: unknown; } interface DirectoryItemData { name: string; path: string; type: string; [key: string]: unknown; } export interface SidebarItem { id: string; name: string; path: string; type: "recent" | "pinned" | "shortcut" | "folder"; lastAccessed?: string; isExpanded?: boolean; children?: SidebarItem[]; } interface FileManagerSidebarProps { currentHost: SSHHost; currentPath: string; onPathChange: (path: string) => void; onFileOpen?: (file: SidebarItem) => void; sshSessionId?: string; refreshTrigger?: number; } export function FileManagerSidebar({ currentHost, currentPath, onPathChange, onFileOpen, sshSessionId, refreshTrigger, }: FileManagerSidebarProps) { const { t } = useTranslation(); const [recentItems, setRecentItems] = useState([]); const [pinnedItems, setPinnedItems] = useState([]); const [shortcuts, setShortcuts] = useState([]); const [directoryTree, setDirectoryTree] = useState([]); const [expandedFolders, setExpandedFolders] = useState>( new Set(["root"]), ); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; isVisible: boolean; item: SidebarItem | null; }>({ x: 0, y: 0, isVisible: false, item: null, }); useEffect(() => { loadQuickAccessData(); }, [currentHost, refreshTrigger]); useEffect(() => { if (sshSessionId) { loadDirectoryTree(); } }, [sshSessionId]); const loadQuickAccessData = async () => { if (!currentHost?.id) return; try { const recentData = await getRecentFiles(currentHost.id); const recentItems = (recentData as RecentFileData[]) .slice(0, 5) .map((item: RecentFileData) => ({ id: `recent-${item.id}`, name: item.name, path: item.path, type: "recent" as const, lastAccessed: item.lastOpened, })); setRecentItems(recentItems); const pinnedData = await getPinnedFiles(currentHost.id); const pinnedItems = (pinnedData as PinnedFileData[]).map( (item: PinnedFileData) => ({ id: `pinned-${item.id}`, name: item.name, path: item.path, type: "pinned" as const, }), ); setPinnedItems(pinnedItems); const shortcutData = await getFolderShortcuts(currentHost.id); const shortcutItems = (shortcutData as ShortcutData[]).map( (item: ShortcutData) => ({ id: `shortcut-${item.id}`, name: item.name, path: item.path, type: "shortcut" as const, }), ); setShortcuts(shortcutItems); } catch (error) { console.error("Failed to load quick access data:", error); setRecentItems([]); setPinnedItems([]); setShortcuts([]); } }; const handleRemoveRecentFile = async (item: SidebarItem) => { if (!currentHost?.id) return; try { await removeRecentFile(currentHost.id, item.path); loadQuickAccessData(); toast.success( t("fileManager.removedFromRecentFiles", { name: item.name }), ); } catch (error) { console.error("Failed to remove recent file:", error); toast.error(t("fileManager.removeFailed")); } }; const handleUnpinFile = async (item: SidebarItem) => { if (!currentHost?.id) return; try { await removePinnedFile(currentHost.id, item.path); loadQuickAccessData(); toast.success(t("fileManager.unpinnedSuccessfully", { name: item.name })); } catch (error) { console.error("Failed to unpin file:", error); toast.error(t("fileManager.unpinFailed")); } }; const handleRemoveShortcut = async (item: SidebarItem) => { if (!currentHost?.id) return; try { await removeFolderShortcut(currentHost.id, item.path); loadQuickAccessData(); toast.success(t("fileManager.removedShortcut", { name: item.name })); } catch (error) { console.error("Failed to remove shortcut:", error); toast.error(t("fileManager.removeShortcutFailed")); } }; const handleClearAllRecent = async () => { if (!currentHost?.id || recentItems.length === 0) return; try { await Promise.all( recentItems.map((item) => removeRecentFile(currentHost.id, item.path)), ); loadQuickAccessData(); toast.success(t("fileManager.clearedAllRecentFiles")); } catch (error) { console.error("Failed to clear recent files:", error); toast.error(t("fileManager.clearFailed")); } }; const handleContextMenu = (e: React.MouseEvent, item: SidebarItem) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY, isVisible: true, item, }); }; const closeContextMenu = () => { setContextMenu((prev) => ({ ...prev, isVisible: false, item: null })); }; useEffect(() => { if (!contextMenu.isVisible) return; const handleClickOutside = (event: MouseEvent) => { const target = event.target as Element; const menuElement = document.querySelector("[data-sidebar-context-menu]"); if (!menuElement?.contains(target)) { closeContextMenu(); } }; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { closeContextMenu(); } }; const timeoutId = setTimeout(() => { document.addEventListener("mousedown", handleClickOutside); document.addEventListener("keydown", handleKeyDown); }, 50); return () => { clearTimeout(timeoutId); document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleKeyDown); }; }, [contextMenu.isVisible]); const loadDirectoryTree = async () => { if (!sshSessionId) return; try { const response = await listSSHFiles(sshSessionId, "/"); const rootFiles = (response.files || []) as DirectoryItemData[]; const rootFolders = rootFiles.filter( (item: DirectoryItemData) => item.type === "directory", ); const rootTreeItems = rootFolders.map((folder: DirectoryItemData) => ({ id: `folder-${folder.name}`, name: folder.name, path: folder.path, type: "folder" as const, isExpanded: false, children: [], })); setDirectoryTree([ { id: "root", name: "/", path: "/", type: "folder" as const, isExpanded: true, children: rootTreeItems, }, ]); } catch (error) { console.error("Failed to load directory tree:", error); setDirectoryTree([ { id: "root", name: "/", path: "/", type: "folder" as const, isExpanded: false, children: [], }, ]); } }; const handleItemClick = (item: SidebarItem) => { if (item.type === "folder") { toggleFolder(item.id, item.path); onPathChange(item.path); } else if (item.type === "recent" || item.type === "pinned") { if (onFileOpen) { onFileOpen(item); } else { const directory = item.path.substring(0, item.path.lastIndexOf("/")) || "/"; onPathChange(directory); } } else if (item.type === "shortcut") { onPathChange(item.path); } }; const toggleFolder = async (folderId: string, folderPath?: string) => { const newExpanded = new Set(expandedFolders); if (newExpanded.has(folderId)) { newExpanded.delete(folderId); } else { newExpanded.add(folderId); if (sshSessionId && folderPath && folderPath !== "/") { try { const subResponse = await listSSHFiles(sshSessionId, folderPath); const subFiles = (subResponse.files || []) as DirectoryItemData[]; const subFolders = subFiles.filter( (item: DirectoryItemData) => item.type === "directory", ); const subTreeItems = subFolders.map((folder: DirectoryItemData) => ({ id: `folder-${folder.path.replace(/\//g, "-")}`, name: folder.name, path: folder.path, type: "folder" as const, isExpanded: false, children: [], })); setDirectoryTree((prevTree) => { const updateChildren = (items: SidebarItem[]): SidebarItem[] => { return items.map((item) => { if (item.id === folderId) { return { ...item, children: subTreeItems }; } else if (item.children) { return { ...item, children: updateChildren(item.children) }; } return item; }); }; return updateChildren(prevTree); }); } catch (error) { console.error("Failed to load subdirectory:", error); } } } setExpandedFolders(newExpanded); }; const renderSidebarItem = (item: SidebarItem, level: number = 0) => { const isExpanded = expandedFolders.has(item.id); const isActive = currentPath === item.path; return (
handleItemClick(item)} onContextMenu={(e) => { if ( item.type === "recent" || item.type === "pinned" || item.type === "shortcut" ) { handleContextMenu(e, item); } }} > {item.type === "folder" && ( )} {item.type === "folder" ? ( isExpanded ? ( ) : ( ) ) : ( )} {item.name}
{item.type === "folder" && isExpanded && item.children && (
{item.children.map((child) => renderSidebarItem(child, level + 1))}
)}
); }; const renderSection = ( title: string, icon: React.ReactNode, items: SidebarItem[], ) => { if (items.length === 0) return null; return (
{icon} {title}
{items.map((item) => renderSidebarItem(item))}
); }; const hasQuickAccessItems = recentItems.length > 0 || pinnedItems.length > 0 || shortcuts.length > 0; return ( <>
{renderSection( t("fileManager.recent"), , recentItems, )} {renderSection( t("fileManager.pinned"), , pinnedItems, )} {renderSection( t("fileManager.folderShortcuts"), , shortcuts, )}
{t("fileManager.directories")}
{directoryTree.map((item) => renderSidebarItem(item))}
{contextMenu.isVisible && contextMenu.item && ( <>
{contextMenu.item.type === "recent" && ( <> {recentItems.length > 1 && ( <>
)} )} {contextMenu.item.type === "pinned" && ( )} {contextMenu.item.type === "shortcut" && ( )}
)} ); }