mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
Improve file manager navigation and compact layout (#1308)
This commit is contained in:
@@ -154,6 +154,11 @@ function FileManagerContent({
|
|||||||
const saved = localStorage.getItem("fileManagerViewMode");
|
const saved = localStorage.getItem("fileManagerViewMode");
|
||||||
return saved === "grid" || saved === "list" ? saved : "grid";
|
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.
|
// Picking an interface preset seeds this key from another part of the app.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
@@ -884,6 +889,25 @@ function FileManagerContent({
|
|||||||
}
|
}
|
||||||
}, [navIndex, navHistory, sshSessionId]);
|
}, [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(() => {
|
const goUp = useCallback(() => {
|
||||||
if (currentPath === "/") return;
|
if (currentPath === "/") return;
|
||||||
const parent =
|
const parent =
|
||||||
@@ -3145,6 +3169,10 @@ function FileManagerContent({
|
|||||||
localStorage.setItem("fileManagerViewMode", viewMode);
|
localStorage.setItem("fileManagerViewMode", viewMode);
|
||||||
}, [viewMode]);
|
}, [viewMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("fileManagerDensity", density);
|
||||||
|
}, [density]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("fileManagerSortBy", sortBy);
|
localStorage.setItem("fileManagerSortBy", sortBy);
|
||||||
localStorage.setItem("fileManagerSortOrder", sortOrder);
|
localStorage.setItem("fileManagerSortOrder", sortOrder);
|
||||||
@@ -3223,6 +3251,8 @@ function FileManagerContent({
|
|||||||
setSearchQuery={setSearchQuery}
|
setSearchQuery={setSearchQuery}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
setViewMode={setViewMode}
|
setViewMode={setViewMode}
|
||||||
|
density={density}
|
||||||
|
setDensity={setDensity}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
setSortBy={setSortBy}
|
setSortBy={setSortBy}
|
||||||
sortOrder={sortOrder}
|
sortOrder={sortOrder}
|
||||||
@@ -3304,6 +3334,7 @@ function FileManagerContent({
|
|||||||
}
|
}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
|
density={density}
|
||||||
onRename={handleRenameConfirm}
|
onRename={handleRenameConfirm}
|
||||||
editingFile={editingFile}
|
editingFile={editingFile}
|
||||||
onStartEdit={handleStartEdit}
|
onStartEdit={handleStartEdit}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ interface FileManagerGridProps {
|
|||||||
onDownload?: (files: FileItem[]) => void;
|
onDownload?: (files: FileItem[]) => void;
|
||||||
onContextMenu?: (event: React.MouseEvent, file?: FileItem) => void;
|
onContextMenu?: (event: React.MouseEvent, file?: FileItem) => void;
|
||||||
viewMode?: "grid" | "list";
|
viewMode?: "grid" | "list";
|
||||||
|
density?: "comfortable" | "compact";
|
||||||
onRename?: (file: FileItem, newName: string) => void;
|
onRename?: (file: FileItem, newName: string) => void;
|
||||||
editingFile?: FileItem | null;
|
editingFile?: FileItem | null;
|
||||||
onStartEdit?: (file: FileItem) => void;
|
onStartEdit?: (file: FileItem) => void;
|
||||||
@@ -89,8 +90,18 @@ const getFileTypeColor = (file: FileItem): string => {
|
|||||||
return "text-blue-400";
|
return "text-blue-400";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFileIcon = (file: FileItem, viewMode: "grid" | "list" = "grid") => {
|
const getFileIcon = (
|
||||||
const iconClass = viewMode === "grid" ? "w-8 h-8" : "w-6 h-6";
|
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);
|
const colorClass = getFileTypeColor(file);
|
||||||
|
|
||||||
if (file.type === "directory") {
|
if (file.type === "directory") {
|
||||||
@@ -170,6 +181,7 @@ export function FileManagerGrid({
|
|||||||
onDownload,
|
onDownload,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
viewMode = "grid",
|
viewMode = "grid",
|
||||||
|
density = "comfortable",
|
||||||
onRename,
|
onRename,
|
||||||
editingFile,
|
editingFile,
|
||||||
onStartEdit,
|
onStartEdit,
|
||||||
@@ -197,10 +209,13 @@ export function FileManagerGrid({
|
|||||||
const [editingName, setEditingName] = useState("");
|
const [editingName, setEditingName] = useState("");
|
||||||
const [gridCols, setGridCols] = useState(4);
|
const [gridCols, setGridCols] = useState(4);
|
||||||
|
|
||||||
const LIST_ROW_H = 41;
|
const compact = density === "compact";
|
||||||
const GRID_ROW_H = 112;
|
const LIST_ROW_H = compact ? 29 : 41;
|
||||||
const LIST_HEADER_H = 33;
|
const GRID_ROW_H = compact ? 76 : 112;
|
||||||
const CONTENT_PAD = 16;
|
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<DragState>({
|
const [dragState, setDragState] = useState<DragState>({
|
||||||
type: "none",
|
type: "none",
|
||||||
@@ -216,15 +231,17 @@ export function FileManagerGrid({
|
|||||||
|
|
||||||
const updateCols = () => {
|
const updateCols = () => {
|
||||||
const w = el.clientWidth - CONTENT_PAD * 2;
|
const w = el.clientWidth - CONTENT_PAD * 2;
|
||||||
// gap-4 (16px) + ~min cell 96px
|
const n = Math.max(
|
||||||
const n = Math.max(2, Math.min(8, Math.floor((w + 16) / 112)));
|
2,
|
||||||
|
Math.min(10, Math.floor((w + GRID_GAP) / GRID_CELL)),
|
||||||
|
);
|
||||||
setGridCols(n);
|
setGridCols(n);
|
||||||
};
|
};
|
||||||
updateCols();
|
updateCols();
|
||||||
const ro = new ResizeObserver(updateCols);
|
const ro = new ResizeObserver(updateCols);
|
||||||
ro.observe(el);
|
ro.observe(el);
|
||||||
return () => ro.disconnect();
|
return () => ro.disconnect();
|
||||||
}, [viewMode]);
|
}, [viewMode, CONTENT_PAD, GRID_GAP, GRID_CELL]);
|
||||||
|
|
||||||
const gridRowCount = useMemo(
|
const gridRowCount = useMemo(
|
||||||
() => (viewMode === "grid" ? Math.ceil(files.length / gridCols) : 0),
|
() => (viewMode === "grid" ? Math.ceil(files.length / gridCols) : 0),
|
||||||
@@ -252,7 +269,14 @@ export function FileManagerGrid({
|
|||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (viewMode === "list") listVirtualizer.measure();
|
if (viewMode === "list") listVirtualizer.measure();
|
||||||
else gridVirtualizer.measure();
|
else gridVirtualizer.measure();
|
||||||
}, [viewMode, files.length, editingFile?.path, createIntent, gridCols]);
|
}, [
|
||||||
|
viewMode,
|
||||||
|
density,
|
||||||
|
files.length,
|
||||||
|
editingFile?.path,
|
||||||
|
createIntent,
|
||||||
|
gridCols,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleGlobalMouseMove = (e: MouseEvent) => {
|
const handleGlobalMouseMove = (e: MouseEvent) => {
|
||||||
@@ -558,7 +582,8 @@ export function FileManagerGrid({
|
|||||||
const contentLeft = selectionBox.left - CONTENT_PAD;
|
const contentLeft = selectionBox.left - CONTENT_PAD;
|
||||||
const contentRight = selectionBox.right - CONTENT_PAD;
|
const contentRight = selectionBox.right - CONTENT_PAD;
|
||||||
const cellW =
|
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 startCol = Math.max(0, Math.floor(contentLeft / cellW));
|
||||||
const endCol = Math.min(
|
const endCol = Math.min(
|
||||||
gridCols - 1,
|
gridCols - 1,
|
||||||
@@ -618,6 +643,7 @@ export function FileManagerGrid({
|
|||||||
files,
|
files,
|
||||||
onSelectionChange,
|
onSelectionChange,
|
||||||
viewMode,
|
viewMode,
|
||||||
|
GRID_GAP,
|
||||||
createIntent,
|
createIntent,
|
||||||
gridCols,
|
gridCols,
|
||||||
gridRowCount,
|
gridRowCount,
|
||||||
@@ -921,7 +947,8 @@ export function FileManagerGrid({
|
|||||||
<div
|
<div
|
||||||
ref={gridRef}
|
ref={gridRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 p-4 overflow-y-auto thin-scrollbar",
|
"absolute inset-0 overflow-y-auto thin-scrollbar",
|
||||||
|
compact ? "p-2" : "p-4",
|
||||||
dragState.type === "external" &&
|
dragState.type === "external" &&
|
||||||
"bg-muted/20 border-2 border-dashed border-primary",
|
"bg-muted/20 border-2 border-dashed border-primary",
|
||||||
)}
|
)}
|
||||||
@@ -956,10 +983,10 @@ export function FileManagerGrid({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : viewMode === "grid" ? (
|
) : viewMode === "grid" ? (
|
||||||
<div className="flex flex-col gap-4">
|
<div className={cn("flex flex-col", compact ? "gap-2" : "gap-4")}>
|
||||||
{createIntent && (
|
{createIntent && (
|
||||||
<div
|
<div
|
||||||
className="grid gap-4"
|
className={cn("grid", compact ? "gap-2" : "gap-4")}
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
|
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
|
||||||
}}
|
}}
|
||||||
@@ -989,7 +1016,10 @@ export function FileManagerGrid({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="grid gap-4 pb-4"
|
className={cn(
|
||||||
|
"grid",
|
||||||
|
compact ? "gap-2 pb-2" : "gap-4 pb-4",
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
|
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,
|
||||||
}}
|
}}
|
||||||
@@ -1004,7 +1034,8 @@ export function FileManagerGrid({
|
|||||||
data-file-path={file.path}
|
data-file-path={file.path}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex flex-col items-center p-3 rounded-none border-2 border-transparent transition-all cursor-pointer hover:bg-muted/50 select-none",
|
"group flex flex-col items-center rounded-none border-2 border-transparent transition-all cursor-pointer hover:bg-muted/50 select-none",
|
||||||
|
compact ? "p-1.5" : "p-3",
|
||||||
isSelected &&
|
isSelected &&
|
||||||
"bg-accent-brand/10 border-accent-brand/40",
|
"bg-accent-brand/10 border-accent-brand/40",
|
||||||
dragState.target?.path === file.path &&
|
dragState.target?.path === file.path &&
|
||||||
@@ -1026,8 +1057,13 @@ export function FileManagerGrid({
|
|||||||
onDrop={(e) => handleFileDrop(e, file)}
|
onDrop={(e) => handleFileDrop(e, file)}
|
||||||
onDragEnd={handleFileDragEnd}
|
onDragEnd={handleFileDragEnd}
|
||||||
>
|
>
|
||||||
<div className="relative mb-2 pointer-events-none">
|
<div
|
||||||
{getFileIcon(file, viewMode)}
|
className={cn(
|
||||||
|
"relative pointer-events-none",
|
||||||
|
compact ? "mb-1" : "mb-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{getFileIcon(file, viewMode, compact)}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex flex-col items-center pointer-events-none">
|
<div className="w-full flex flex-col items-center pointer-events-none">
|
||||||
{editingFile?.path === file.path ? (
|
{editingFile?.path === file.path ? (
|
||||||
@@ -1046,7 +1082,10 @@ export function FileManagerGrid({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p
|
||||||
className="text-[11px] font-bold tracking-tight text-center truncate w-full px-1"
|
className={cn(
|
||||||
|
"font-bold tracking-tight text-center truncate w-full px-1",
|
||||||
|
compact ? "text-[10px]" : "text-[11px]",
|
||||||
|
)}
|
||||||
title={file.name}
|
title={file.name}
|
||||||
>
|
>
|
||||||
{file.name}
|
{file.name}
|
||||||
@@ -1079,7 +1118,12 @@ export function FileManagerGrid({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="grid grid-cols-[1fr_120px_150px_80px_90px] gap-2 px-4 py-2 text-[10px] font-bold uppercase tracking-widest text-muted-foreground border-b border-border sticky top-0 bg-card z-10">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid grid-cols-[1fr_120px_150px_80px_90px] gap-2 text-[10px] font-bold uppercase tracking-widest text-muted-foreground border-b border-border bg-card",
|
||||||
|
compact ? "px-2 py-1" : "px-4 py-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 cursor-pointer hover:text-accent-brand transition-colors"
|
className="flex items-center gap-1 cursor-pointer hover:text-accent-brand transition-colors"
|
||||||
onClick={() => onSortChange?.("name")}
|
onClick={() => onSortChange?.("name")}
|
||||||
@@ -1150,7 +1194,10 @@ export function FileManagerGrid({
|
|||||||
data-file-path={file.path}
|
data-file-path={file.path}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
className={cn(
|
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",
|
isSelected && "bg-accent-brand/10",
|
||||||
dragState.target?.path === file.path &&
|
dragState.target?.path === file.path &&
|
||||||
"bg-accent-brand/20 border-accent-brand border-dashed",
|
"bg-accent-brand/20 border-accent-brand border-dashed",
|
||||||
@@ -1169,9 +1216,14 @@ export function FileManagerGrid({
|
|||||||
onDrop={(e) => handleFileDrop(e, file)}
|
onDrop={(e) => handleFileDrop(e, file)}
|
||||||
onDragEnd={handleFileDragEnd}
|
onDragEnd={handleFileDragEnd}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 overflow-hidden pointer-events-none">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center overflow-hidden pointer-events-none",
|
||||||
|
compact ? "gap-2" : "gap-3",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
{getFileIcon(file, viewMode)}
|
{getFileIcon(file, viewMode, compact)}
|
||||||
</div>
|
</div>
|
||||||
{editingFile?.path === file.path ? (
|
{editingFile?.path === file.path ? (
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
List,
|
List,
|
||||||
Plus,
|
Plus,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
Rows3,
|
||||||
Search,
|
Search,
|
||||||
Trash2,
|
Trash2,
|
||||||
Upload,
|
Upload,
|
||||||
@@ -33,6 +34,7 @@ import type { FileItem } from "@/types/index";
|
|||||||
type SortBy = "name" | "modified" | "size";
|
type SortBy = "name" | "modified" | "size";
|
||||||
type SortOrder = "asc" | "desc";
|
type SortOrder = "asc" | "desc";
|
||||||
type ViewMode = "grid" | "list";
|
type ViewMode = "grid" | "list";
|
||||||
|
type Density = "comfortable" | "compact";
|
||||||
|
|
||||||
type FileManagerToolbarProps = {
|
type FileManagerToolbarProps = {
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
@@ -46,6 +48,8 @@ type FileManagerToolbarProps = {
|
|||||||
setSearchQuery: (query: string) => void;
|
setSearchQuery: (query: string) => void;
|
||||||
viewMode: ViewMode;
|
viewMode: ViewMode;
|
||||||
setViewMode: (mode: ViewMode) => void;
|
setViewMode: (mode: ViewMode) => void;
|
||||||
|
density: Density;
|
||||||
|
setDensity: (density: Density) => void;
|
||||||
sortBy: SortBy;
|
sortBy: SortBy;
|
||||||
setSortBy: (sortBy: SortBy) => void;
|
setSortBy: (sortBy: SortBy) => void;
|
||||||
sortOrder: SortOrder;
|
sortOrder: SortOrder;
|
||||||
@@ -197,6 +201,8 @@ export function FileManagerToolbar({
|
|||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
viewMode,
|
viewMode,
|
||||||
setViewMode,
|
setViewMode,
|
||||||
|
density,
|
||||||
|
setDensity,
|
||||||
sortBy,
|
sortBy,
|
||||||
setSortBy,
|
setSortBy,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
@@ -321,6 +327,21 @@ export function FileManagerToolbar({
|
|||||||
>
|
>
|
||||||
<List className="size-4" />
|
<List className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={density === "compact" ? "secondary" : "ghost"}
|
||||||
|
size="icon"
|
||||||
|
onClick={() =>
|
||||||
|
setDensity(density === "compact" ? "comfortable" : "compact")
|
||||||
|
}
|
||||||
|
className={`size-8 rounded-none border-y-0 border-r-0 border-l border-border ${density === "compact" ? "bg-accent-brand/10 text-accent-brand" : ""}`}
|
||||||
|
title={t(
|
||||||
|
density === "compact"
|
||||||
|
? "fileManager.comfortableLayout"
|
||||||
|
: "fileManager.compactLayout",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Rows3 className="size-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -2283,6 +2283,8 @@
|
|||||||
"used": "Used",
|
"used": "Used",
|
||||||
"of": "of",
|
"of": "of",
|
||||||
"toggleSidebar": "Toggle Sidebar",
|
"toggleSidebar": "Toggle Sidebar",
|
||||||
|
"compactLayout": "Use compact layout",
|
||||||
|
"comfortableLayout": "Use comfortable layout",
|
||||||
"cannotLoadPdf": "Cannot load PDF",
|
"cannotLoadPdf": "Cannot load PDF",
|
||||||
"pdfLoadError": "There was an error loading this PDF file.",
|
"pdfLoadError": "There was an error loading this PDF file.",
|
||||||
"loadingPdf": "Loading PDF...",
|
"loadingPdf": "Loading PDF...",
|
||||||
|
|||||||
Reference in New Issue
Block a user