From 59407278d1d20bcffce04c0959fd3d9a1005cb83 Mon Sep 17 00:00:00 2001 From: Elijah Date: Sat, 23 May 2026 17:00:09 -0700 Subject: [PATCH] fix(ui): improve drag selection scroll behavior, add right-click to unpin sidebar items, close preview on back navigation, fix shared files alignment --- frontend/src/components/FileExplorer.tsx | 69 +++++++++++++++------ frontend/src/components/SharedFilesPage.tsx | 2 +- 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/FileExplorer.tsx b/frontend/src/components/FileExplorer.tsx index 683f7fd..8b66290 100644 --- a/frontend/src/components/FileExplorer.tsx +++ b/frontend/src/components/FileExplorer.tsx @@ -220,6 +220,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { const handlePopState = (e: PopStateEvent) => { if (e.state && e.state.path) setCurrentPath(e.state.path); else setCurrentPath(new URLSearchParams(window.location.search).get('path') || '.'); + setPreviewFile(null); }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); @@ -273,7 +274,9 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { const [pinnedOrder, setPinnedOrder] = useState([]); const [dragOverPinnedItem, setDragOverPinnedItem] = useState(null); const [pinnedRefreshCounter, setPinnedRefreshCounter] = useState(0); + const [selectedFiles, setSelectedFiles] = useState>(new Set()); const [selectionBox, setSelectionBox] = useState<{ startX: number; startY: number; currentX: number; currentY: number } | null>(null); + const lastMousePos = useRef<{clientX: number, clientY: number} | null>(null); const [initialSelectedFiles, setInitialSelectedFiles] = useState>(new Set()); const searchTimeoutRef = useRef(undefined); const [downloadToken, setDownloadToken] = useState(''); @@ -527,12 +530,18 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { if (target.closest('button') || target.closest('input') || target.closest('[data-file-item]')) { return; } + const rect = mainContainerRef.current!.getBoundingClientRect(); + const scrollLeft = mainContainerRef.current!.scrollLeft; + const scrollTop = mainContainerRef.current!.scrollTop; + setSelectionBox({ - startX: e.clientX, - startY: e.clientY, - currentX: e.clientX, - currentY: e.clientY, + startX: e.clientX - rect.left + scrollLeft, + startY: e.clientY - rect.top + scrollTop, + currentX: e.clientX - rect.left + scrollLeft, + currentY: e.clientY - rect.top + scrollTop, }); + lastMousePos.current = { clientX: e.clientX, clientY: e.clientY }; + if (e.shiftKey || e.ctrlKey || e.metaKey) { setInitialSelectedFiles(new Set(selectedFiles)); } else { @@ -541,26 +550,35 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { } } - function handlePointerMove(e: React.PointerEvent) { - if (!selectionBox) return; - setSelectionBox(prev => prev ? { ...prev, currentX: e.clientX, currentY: e.clientY } : null); + function updateSelection(clientX: number, clientY: number) { + setSelectionBox(prev => { + if (!prev || !mainContainerRef.current) return null; + const rect = mainContainerRef.current.getBoundingClientRect(); + const scrollLeft = mainContainerRef.current.scrollLeft; + const scrollTop = mainContainerRef.current.scrollTop; + + const next = { ...prev, currentX: clientX - rect.left + scrollLeft, currentY: clientY - rect.top + scrollTop }; - if (mainContainerRef.current) { - const boxLeft = Math.min(selectionBox.startX, e.clientX); - const boxRight = Math.max(selectionBox.startX, e.clientX); - const boxTop = Math.min(selectionBox.startY, e.clientY); - const boxBottom = Math.max(selectionBox.startY, e.clientY); + const boxLeft = Math.min(next.startX, next.currentX); + const boxRight = Math.max(next.startX, next.currentX); + const boxTop = Math.min(next.startY, next.currentY); + const boxBottom = Math.max(next.startY, next.currentY); + + const viewportBoxLeft = boxLeft + rect.left - scrollLeft; + const viewportBoxRight = boxRight + rect.left - scrollLeft; + const viewportBoxTop = boxTop + rect.top - scrollTop; + const viewportBoxBottom = boxBottom + rect.top - scrollTop; const fileNodes = mainContainerRef.current.querySelectorAll('[data-file-item]'); const newSelected = new Set(initialSelectedFiles); fileNodes.forEach(node => { - const rect = node.getBoundingClientRect(); + const fileRect = node.getBoundingClientRect(); const intersects = !( - rect.right < boxLeft || - rect.left > boxRight || - rect.bottom < boxTop || - rect.top > boxBottom + fileRect.right < viewportBoxLeft || + fileRect.left > viewportBoxRight || + fileRect.bottom < viewportBoxTop || + fileRect.top > viewportBoxBottom ); const filePath = node.getAttribute('data-file-path'); @@ -574,6 +592,19 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { }); setSelectedFiles(newSelected); + return next; + }); + } + + function handlePointerMove(e: React.PointerEvent) { + if (!selectionBox) return; + lastMousePos.current = { clientX: e.clientX, clientY: e.clientY }; + updateSelection(e.clientX, e.clientY); + } + + function handleScroll(e: React.UIEvent) { + if (selectionBox && lastMousePos.current) { + updateSelection(lastMousePos.current.clientX, lastMousePos.current.clientY); } } @@ -1270,6 +1301,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { borderTop: dragOverPinnedItem === file.path ? '2px solid var(--color-accent)' : '2px solid transparent', }} onClick={() => { setActiveSection('files'); navigateTo(file.path); }} + onContextMenu={(e) => handleContextMenu(e, file)} >
{getFileIcon(file)} @@ -1490,10 +1522,11 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) { onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onPointerLeave={handlePointerUp} + onScroll={handleScroll} > {selectionBox && (
-
+

Shared Files

{shares.length === 0 ? (