diff --git a/src/ui/features/file-manager/FileManager.tsx b/src/ui/features/file-manager/FileManager.tsx index 22a900a9..0dfe450c 100644 --- a/src/ui/features/file-manager/FileManager.tsx +++ b/src/ui/features/file-manager/FileManager.tsx @@ -154,6 +154,11 @@ function FileManagerContent({ const saved = localStorage.getItem("fileManagerViewMode"); return saved === "grid" || saved === "list" ? saved : "grid"; }); + const [density, setDensity] = useState<"comfortable" | "compact">(() => + localStorage.getItem("fileManagerDensity") === "compact" + ? "compact" + : "comfortable", + ); // Picking an interface preset seeds this key from another part of the app. useEffect(() => { const handler = () => { @@ -884,6 +889,25 @@ function FileManagerContent({ } }, [navIndex, navHistory, sshSessionId]); + useEffect(() => { + const handleMouseNavigation = (event: MouseEvent) => { + if (event.button !== 3 && event.button !== 4) return; + event.preventDefault(); + if (event.button === 3) goBack(); + else goForward(); + }; + const preventBrowserMouseNavigation = (event: MouseEvent) => { + if (event.button === 3 || event.button === 4) event.preventDefault(); + }; + + window.addEventListener("mouseup", handleMouseNavigation); + window.addEventListener("auxclick", preventBrowserMouseNavigation); + return () => { + window.removeEventListener("mouseup", handleMouseNavigation); + window.removeEventListener("auxclick", preventBrowserMouseNavigation); + }; + }, [goBack, goForward]); + const goUp = useCallback(() => { if (currentPath === "/") return; const parent = @@ -3145,6 +3169,10 @@ function FileManagerContent({ localStorage.setItem("fileManagerViewMode", viewMode); }, [viewMode]); + useEffect(() => { + localStorage.setItem("fileManagerDensity", density); + }, [density]); + useEffect(() => { localStorage.setItem("fileManagerSortBy", sortBy); localStorage.setItem("fileManagerSortOrder", sortOrder); @@ -3223,6 +3251,8 @@ function FileManagerContent({ setSearchQuery={setSearchQuery} viewMode={viewMode} setViewMode={setViewMode} + density={density} + setDensity={setDensity} sortBy={sortBy} setSortBy={setSortBy} sortOrder={sortOrder} @@ -3304,6 +3334,7 @@ function FileManagerContent({ } onContextMenu={handleContextMenu} viewMode={viewMode} + density={density} onRename={handleRenameConfirm} editingFile={editingFile} onStartEdit={handleStartEdit} diff --git a/src/ui/features/file-manager/FileManagerGrid.tsx b/src/ui/features/file-manager/FileManagerGrid.tsx index e5e43490..cef4dc66 100644 --- a/src/ui/features/file-manager/FileManagerGrid.tsx +++ b/src/ui/features/file-manager/FileManagerGrid.tsx @@ -53,6 +53,7 @@ interface FileManagerGridProps { onDownload?: (files: FileItem[]) => void; onContextMenu?: (event: React.MouseEvent, file?: FileItem) => void; viewMode?: "grid" | "list"; + density?: "comfortable" | "compact"; onRename?: (file: FileItem, newName: string) => void; editingFile?: FileItem | null; onStartEdit?: (file: FileItem) => void; @@ -89,8 +90,18 @@ const getFileTypeColor = (file: FileItem): string => { return "text-blue-400"; }; -const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => { - const iconClass = viewMode === "grid" ? "w-8 h-8" : "w-6 h-6"; +const getFileIcon = ( + file: FileItem, + viewMode: "grid" | "list" = "grid", + compact = false, +) => { + const iconClass = compact + ? viewMode === "grid" + ? "size-6" + : "size-4" + : viewMode === "grid" + ? "size-8" + : "size-6"; const colorClass = getFileTypeColor(file); if (file.type === "directory") { @@ -170,6 +181,7 @@ export function FileManagerGrid({ onDownload, onContextMenu, viewMode = "grid", + density = "comfortable", onRename, editingFile, onStartEdit, @@ -197,10 +209,13 @@ export function FileManagerGrid({ const [editingName, setEditingName] = useState(""); const [gridCols, setGridCols] = useState(4); - const LIST_ROW_H = 41; - const GRID_ROW_H = 112; - const LIST_HEADER_H = 33; - const CONTENT_PAD = 16; + const compact = density === "compact"; + const LIST_ROW_H = compact ? 29 : 41; + const GRID_ROW_H = compact ? 76 : 112; + const LIST_HEADER_H = compact ? 25 : 33; + const CONTENT_PAD = compact ? 8 : 16; + const GRID_GAP = compact ? 8 : 16; + const GRID_CELL = compact ? 76 : 112; const [dragState, setDragState] = useState({ type: "none", @@ -216,15 +231,17 @@ export function FileManagerGrid({ const updateCols = () => { const w = el.clientWidth - CONTENT_PAD * 2; - // gap-4 (16px) + ~min cell 96px - const n = Math.max(2, Math.min(8, Math.floor((w + 16) / 112))); + const n = Math.max( + 2, + Math.min(10, Math.floor((w + GRID_GAP) / GRID_CELL)), + ); setGridCols(n); }; updateCols(); const ro = new ResizeObserver(updateCols); ro.observe(el); return () => ro.disconnect(); - }, [viewMode]); + }, [viewMode, CONTENT_PAD, GRID_GAP, GRID_CELL]); const gridRowCount = useMemo( () => (viewMode === "grid" ? Math.ceil(files.length / gridCols) : 0), @@ -252,7 +269,14 @@ export function FileManagerGrid({ useLayoutEffect(() => { if (viewMode === "list") listVirtualizer.measure(); else gridVirtualizer.measure(); - }, [viewMode, files.length, editingFile?.path, createIntent, gridCols]); + }, [ + viewMode, + density, + files.length, + editingFile?.path, + createIntent, + gridCols, + ]); useEffect(() => { const handleGlobalMouseMove = (e: MouseEvent) => { @@ -558,7 +582,8 @@ export function FileManagerGrid({ const contentLeft = selectionBox.left - CONTENT_PAD; const contentRight = selectionBox.right - CONTENT_PAD; const cellW = - (gridRef.current.clientWidth - CONTENT_PAD * 2 + 16) / gridCols; + (gridRef.current.clientWidth - CONTENT_PAD * 2 + GRID_GAP) / + gridCols; const startCol = Math.max(0, Math.floor(contentLeft / cellW)); const endCol = Math.min( gridCols - 1, @@ -618,6 +643,7 @@ export function FileManagerGrid({ files, onSelectionChange, viewMode, + GRID_GAP, createIntent, gridCols, gridRowCount, @@ -921,7 +947,8 @@ export function FileManagerGrid({
) : viewMode === "grid" ? ( -
+
{createIntent && (
handleFileDrop(e, file)} onDragEnd={handleFileDragEnd} > -
- {getFileIcon(file, viewMode)} +
+ {getFileIcon(file, viewMode, compact)}
{editingFile?.path === file.path ? ( @@ -1046,7 +1082,10 @@ export function FileManagerGrid({ /> ) : (

{file.name} @@ -1079,7 +1118,12 @@ export function FileManagerGrid({

) : (
-
+
onSortChange?.("name")} @@ -1150,7 +1194,10 @@ export function FileManagerGrid({ data-file-path={file.path} draggable={true} className={cn( - "grid grid-cols-[1fr_120px_150px_80px_90px] gap-2 px-4 py-2 items-center text-xs cursor-pointer border-b border-border hover:bg-muted/50 rounded-none select-none transition-colors", + "grid grid-cols-[1fr_120px_150px_80px_90px] gap-2 items-center cursor-pointer border-b border-border hover:bg-muted/50 rounded-none select-none transition-colors", + compact + ? "px-2 py-1 text-[11px]" + : "px-4 py-2 text-xs", isSelected && "bg-accent-brand/10", dragState.target?.path === file.path && "bg-accent-brand/20 border-accent-brand border-dashed", @@ -1169,9 +1216,14 @@ export function FileManagerGrid({ onDrop={(e) => handleFileDrop(e, file)} onDragEnd={handleFileDragEnd} > -
+
- {getFileIcon(file, viewMode)} + {getFileIcon(file, viewMode, compact)}
{editingFile?.path === file.path ? ( string; @@ -46,6 +48,8 @@ type FileManagerToolbarProps = { setSearchQuery: (query: string) => void; viewMode: ViewMode; setViewMode: (mode: ViewMode) => void; + density: Density; + setDensity: (density: Density) => void; sortBy: SortBy; setSortBy: (sortBy: SortBy) => void; sortOrder: SortOrder; @@ -197,6 +201,8 @@ export function FileManagerToolbar({ setSearchQuery, viewMode, setViewMode, + density, + setDensity, sortBy, setSortBy, sortOrder, @@ -321,6 +327,21 @@ export function FileManagerToolbar({ > +