From dc6ff70b81cd6068816789350c9e93557ae8b28d Mon Sep 17 00:00:00 2001 From: Weaver Goldman Date: Wed, 7 Aug 2024 18:04:50 -0400 Subject: [PATCH] Add drag and drop folders and files to open new directory. --- .../components/FileExplorer/FileExplorer.tsx | 79 +++++++++++++++++-- .../src/contexts/DirectoryContext.tsx | 27 ++++--- src/renderer/src/interfaces/file.tsx | 18 +++++ 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/src/renderer/src/components/MenuPanel/components/FileExplorer/FileExplorer.tsx b/src/renderer/src/components/MenuPanel/components/FileExplorer/FileExplorer.tsx index fb51cc9..d88eea0 100644 --- a/src/renderer/src/components/MenuPanel/components/FileExplorer/FileExplorer.tsx +++ b/src/renderer/src/components/MenuPanel/components/FileExplorer/FileExplorer.tsx @@ -2,17 +2,25 @@ import Header from '@renderer/components/Header/Header' import styles from './FileExplorer.module.css' import FileEntry from './components/FileEntry/FileEntry' import { DragOverlay, useDroppable } from '@dnd-kit/core' -import { MouseEvent, useCallback, useContext, useEffect, useState } from 'react' +import { MouseEvent, useCallback, useContext, useEffect, useRef, useState } from 'react' import DraggableEntry from './components/DraggableEntry/DraggableEntry' import { DragContext } from '@renderer/contexts/DragContext' import { DirectoryContext, FileSystemNode } from '@renderer/contexts/DirectoryContext' import useContextMenu from '@renderer/hooks/use-context-menu' import ContextMenu, { ContextMenuAction } from '@renderer/components/ContextMenu/ContextMenu' -import { deleteFSItem, join, moveFSItem, newFolder, renameFSItem } from '@renderer/interfaces/file' +import { + deleteFSItem, + join, + moveFSItem, + newFolder, + renameFSItem, + isPathFile, + directory +} from '@renderer/interfaces/file' function FileExplorer(): JSX.Element { const { clearDragEvent, parentChildData, active } = useContext(DragContext) - const { nodes, directoryName, directoryPath } = useContext(DirectoryContext) + const { nodes, directoryName, directoryPath, setDirectory } = useContext(DirectoryContext) const { point, clicked, data, handleContextMenu } = useContextMenu() const [renamePath, setRenamePath] = useState(null) @@ -144,13 +152,72 @@ function FileExplorer(): JSX.Element { handleDrop() }, [isOver, parentChildData, directoryPath]) + const dropFileRef = useRef(null) + + // Set the drop node ref + useEffect(() => { + setDropNodeRef(dropFileRef.current) + }, [dropFileRef]) + + const [customDragOver, setCustomDragOver] = useState(false) + + useEffect(() => { + const handleDrop = async (event: DragEvent): Promise => { + event.preventDefault() + + setCustomDragOver(false) + + if (!event.dataTransfer) return + + const files = event.dataTransfer.files + + // Determine if the dropped item is a folder + const item = files[0] + + const isFolder = item.type === '' || !isPathFile(item.path) + + if (isFolder) { + // Send the folder to the main process + setDirectory(item.path) + } else { + // Get the folder path + const folderPath = directory(item.path) + + setDirectory(folderPath) + } + } + + const handleDragOver = (event: DragEvent): void => { + event.preventDefault() + setCustomDragOver(true) + } + const handleDragLeave = (event: DragEvent): void => { + event.preventDefault() + setCustomDragOver(false) + } + + if (dropFileRef.current) { + dropFileRef.current.addEventListener('drop', handleDrop) + dropFileRef.current.addEventListener('dragover', handleDragOver) + dropFileRef.current.addEventListener('dragleave', handleDragLeave) + } + + return (): void => { + if (dropFileRef.current) { + dropFileRef.current.removeEventListener('drop', handleDrop) + dropFileRef.current.removeEventListener('dragover', handleDragOver) + dropFileRef.current.removeEventListener('dragleave', handleDragLeave) + } + } + }, [directoryPath, dropFileRef]) + return ( <> {clicked && }
{ if (!directoryName || !directoryPath) return @@ -165,12 +232,12 @@ function FileExplorer(): JSX.Element { > {directoryName ? ( <> -
+
{fileEntries}
) : ( <> -
+
File > Open Folder
diff --git a/src/renderer/src/contexts/DirectoryContext.tsx b/src/renderer/src/contexts/DirectoryContext.tsx index 559ab42..7bac442 100644 --- a/src/renderer/src/contexts/DirectoryContext.tsx +++ b/src/renderer/src/contexts/DirectoryContext.tsx @@ -5,6 +5,7 @@ export type DirectoryContextValue = { nodes: NodeChildren directoryPath: string | null directoryName: string | null + setDirectory: (directory: string) => void } export const DirectoryContext = createContext(null as never) @@ -37,20 +38,24 @@ function DirectoryProvider({ children }: { children: React.ReactNode }): JSX.Ele const [directoryPath, setDirectoryPath] = useState(null) const [nodes, setNodes] = useState({}) + const setDirectory = useCallback((directory: string): void => { + if (!directory || directory.length === 0) return + + setDirectoryPath(directory) + + // Clean up the directory name + const directorySplit = directory.split(/[/\\]/) + setDirectoryName(directorySplit[directorySplit.length - 1]) + + // Clear the folder contents + setNodes({}) + }, []) + useEffect(() => { const clearSelectedFolderListener = window.electron.ipcRenderer.on( 'selected-folder', (_, directory) => { - if (!directory || directory.length === 0) return - - setDirectoryPath(directory) - - // Clean up the directory name - const directorySplit = directory.split(/[/\\]/) - setDirectoryName(directorySplit[directorySplit.length - 1]) - - // Clear the folder contents - setNodes({}) + setDirectory(directory) } ) @@ -76,7 +81,7 @@ function DirectoryProvider({ children }: { children: React.ReactNode }): JSX.Ele }, [directoryPath]) return ( - + {children} ) diff --git a/src/renderer/src/interfaces/file.tsx b/src/renderer/src/interfaces/file.tsx index a73b94a..5647ddc 100644 --- a/src/renderer/src/interfaces/file.tsx +++ b/src/renderer/src/interfaces/file.tsx @@ -29,3 +29,21 @@ export const renameFSItem = async (oldPath: string, newPath: string): Promise => { return renameFSItem(oldPath, newPath) } + +export const assumeSeparator = (path: string): string => { + return path.includes('/') ? '/' : '\\' +} + +export const basename = (path: string): string => { + return path.split(assumeSeparator(path)).pop() || '' +} + +export const directory = (path: string): string => { + const separator = assumeSeparator(path) + + return path.split(separator).slice(0, -1).join(separator) +} + +export const isPathFile = (path: string): boolean => { + return basename(path).includes('.') +}