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
All checks were successful
Automated Container Build / build-and-push (push) Successful in 32s
This commit is contained in:
parent
99e0cd5f0c
commit
3802d978ed
2 changed files with 100 additions and 5 deletions
|
|
@ -111,7 +111,10 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
||||||
const [pinnedExpanded, setPinnedExpanded] = useState(false);
|
const [pinnedExpanded, setPinnedExpanded] = useState(false);
|
||||||
const [pinnedOrder, setPinnedOrder] = useState<string[]>([]);
|
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 searchTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||||
|
const mainContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [gridSize, setGridSize] = useState('200');
|
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) {
|
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;
|
if ((e.target as HTMLElement).closest('[data-file-item]')) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setContextMenu(null);
|
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) {
|
function handleContextMenu(e: React.MouseEvent, file: FileItem) {
|
||||||
e.preventDefault();
|
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) {
|
async function handleSearch(query: string) {
|
||||||
|
|
@ -1027,7 +1097,30 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* File Area */}
|
{/* 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 */}
|
{/* Drag and drop overlay */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{dragOver && (
|
{dragOver && (
|
||||||
|
|
@ -1122,6 +1215,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
<motion.div
|
<motion.div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
data-file-item
|
data-file-item
|
||||||
|
data-file-path={file.path}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
||||||
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
||||||
|
|
@ -1215,6 +1309,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
|
||||||
<motion.div
|
<motion.div
|
||||||
key={file.path}
|
key={file.path}
|
||||||
data-file-item
|
data-file-item
|
||||||
|
data-file-path={file.path}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
onDragStart={(e: any) => handleItemDragStart(e, file.path)}
|
||||||
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
onDragOver={(e: any) => handleItemDragOver(e, file)}
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export default function FilePreview({ file, onClose }: FilePreviewProps) {
|
||||||
src={downloadUrl}
|
src={downloadUrl}
|
||||||
controls
|
controls
|
||||||
autoPlay
|
autoPlay
|
||||||
className="max-w-full max-h-full rounded-lg outline-none"
|
className="max-w-[calc(95vw-2rem)] max-h-[calc(95vh-6rem)] rounded-lg outline-none"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Reference in a new issue