Implement lasso selection, fix context menu positioning, fix video preview sizing
All checks were successful
Automated Container Build / build-and-push (push) Successful in 32s

This commit is contained in:
Elijah 2026-05-23 07:59:52 -07:00
parent 99e0cd5f0c
commit 3802d978ed
2 changed files with 100 additions and 5 deletions

View file

@ -111,7 +111,10 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
const [pinnedExpanded, setPinnedExpanded] = useState(false);
const [pinnedOrder, setPinnedOrder] = useState<string[]>([]);
const [selectionBox, setSelectionBox] = useState<{ startX: number; startY: number; currentX: number; currentY: number } | null>(null);
const [initialSelectedFiles, setInitialSelectedFiles] = useState<Set<string>>(new Set());
const searchTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const mainContainerRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [gridSize, setGridSize] = useState('200');
@ -357,17 +360,84 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
}
}
function handlePointerDown(e: React.PointerEvent) {
if (e.button !== 0) return;
const target = e.target as HTMLElement;
if (target.closest('button') || target.closest('input') || target.closest('[data-file-item]')) {
return;
}
setSelectionBox({
startX: e.clientX,
startY: e.clientY,
currentX: e.clientX,
currentY: e.clientY,
});
if (e.shiftKey || e.ctrlKey || e.metaKey) {
setInitialSelectedFiles(new Set(selectedFiles));
} else {
setInitialSelectedFiles(new Set());
setSelectedFiles(new Set());
}
}
function handlePointerMove(e: React.PointerEvent) {
if (!selectionBox) return;
setSelectionBox(prev => prev ? { ...prev, currentX: e.clientX, currentY: e.clientY } : null);
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 fileNodes = mainContainerRef.current.querySelectorAll('[data-file-item]');
const newSelected = new Set(initialSelectedFiles);
fileNodes.forEach(node => {
const rect = node.getBoundingClientRect();
const intersects = !(
rect.right < boxLeft ||
rect.left > boxRight ||
rect.bottom < boxTop ||
rect.top > boxBottom
);
const filePath = node.getAttribute('data-file-path');
if (filePath) {
if (intersects) {
newSelected.add(filePath);
} else if (!initialSelectedFiles.has(filePath)) {
newSelected.delete(filePath);
}
}
});
setSelectedFiles(newSelected);
}
}
function handlePointerUp() {
setSelectionBox(null);
}
function handleBgContextMenu(e: React.MouseEvent) {
// Only show if the click was on the container itself, not on a file item
if ((e.target as HTMLElement).closest('[data-file-item]')) return;
e.preventDefault();
setContextMenu(null);
setBgContextMenu({ x: e.clientX, y: e.clientY });
setBgContextMenu({
x: Math.min(e.clientX, typeof window !== 'undefined' ? window.innerWidth - 200 : e.clientX),
y: Math.min(e.clientY, typeof window !== 'undefined' ? window.innerHeight - 100 : e.clientY)
});
}
function handleContextMenu(e: React.MouseEvent, file: FileItem) {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, file });
const estimatedHeight = activeSection === 'trash' ? 100 : 320;
setContextMenu({
x: Math.min(e.clientX, typeof window !== 'undefined' ? window.innerWidth - 200 : e.clientX),
y: Math.min(e.clientY, typeof window !== 'undefined' ? window.innerHeight - estimatedHeight : e.clientY),
file
});
}
async function handleSearch(query: string) {
@ -1027,7 +1097,30 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
) : (
<div className="flex flex-1 overflow-hidden">
{/* File Area */}
<div className="flex-1 overflow-y-auto p-6 relative" onContextMenu={activeSection === 'files' ? handleBgContextMenu : undefined}>
<div
className="flex-1 overflow-y-auto p-6 relative"
onContextMenu={activeSection === 'files' ? handleBgContextMenu : undefined}
ref={mainContainerRef}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerLeave={handlePointerUp}
>
{/* Lasso selection box */}
{selectionBox && (
<div
className="fixed z-50 pointer-events-none"
style={{
left: Math.min(selectionBox.startX, selectionBox.currentX),
top: Math.min(selectionBox.startY, selectionBox.currentY),
width: Math.abs(selectionBox.currentX - selectionBox.startX),
height: Math.abs(selectionBox.currentY - selectionBox.startY),
backgroundColor: 'rgba(59, 130, 246, 0.2)',
border: '1px solid rgba(59, 130, 246, 0.5)',
}}
/>
)}
{/* Drag and drop overlay */}
<AnimatePresence>
{dragOver && (
@ -1122,6 +1215,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<motion.div
key={file.path}
data-file-item
data-file-path={file.path}
draggable
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
onDragOver={(e: any) => handleItemDragOver(e, file)}
@ -1215,6 +1309,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<motion.div
key={file.path}
data-file-item
data-file-path={file.path}
draggable
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
onDragOver={(e: any) => handleItemDragOver(e, file)}