diff --git a/docs/changelogs/cinder-v0.2.md b/docs/changelogs/cinder-v0.2.md index 6b07394..c0a4b46 100644 --- a/docs/changelogs/cinder-v0.2.md +++ b/docs/changelogs/cinder-v0.2.md @@ -107,4 +107,3 @@ Allow dragging `.md` files from Finder/Explorer into the workspace to import the ### 10. Note Pinning Allow users to pin frequently accessed notes to the top of the explorer or tabs. - diff --git a/package.json b/package.json index 752f04f..5b51c3e 100644 --- a/package.json +++ b/package.json @@ -58,4 +58,4 @@ "typescript-eslint": "^8.46.4", "vite": "^7.2.4" } -} \ No newline at end of file +} diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 52d3f50..172f770 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -1,6 +1,8 @@ import { useRef, useCallback, useState } from 'react'; +import { invoke } from '@tauri-apps/api/core'; // import { ActivityBar } from '../features/activity-bar/ActivityBar'; import { useAppStore } from '../../store/useAppStore'; +import { useWorkspace } from '../../hooks/useWorkspace'; interface MainLayoutProps { sidebarContent: React.ReactNode; @@ -13,11 +15,15 @@ export function MainLayout({ sidebarContent, editorContent }: MainLayoutProps) { setExplorerCollapsed, sidebarWidth, setSidebarWidth, + isDraggingFiles, + setDraggingFiles, + workspacePath, } = useAppStore(); const isResizingRef = useRef(false); const sidebarRef = useRef(null); const [isResizing, setIsResizing] = useState(false); + const { refreshWorkspace } = useWorkspace(); const startResizing = useCallback( (e: React.MouseEvent) => { @@ -70,14 +76,104 @@ export function MainLayout({ sidebarContent, editorContent }: MainLayoutProps) { setExplorerCollapsed(!isExplorerCollapsed); }; + const handleWindowDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) { + setDraggingFiles(true); + } + }; + + const handleWindowDragOver = (e: React.DragEvent) => { + e.preventDefault(); + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) { + e.dataTransfer.dropEffect = 'copy'; + if (!isDraggingFiles) { + setDraggingFiles(true); + } + } + }; + + const handleWindowDragLeave = (e: React.DragEvent) => { + // Only hide when leaving the window entirely + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + setDraggingFiles(false); + } + }; + + const handleWindowDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDraggingFiles(false); + + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + + if (hasFiles && e.dataTransfer.files && e.dataTransfer.files.length > 0) { + if (!workspacePath) return; + + const filesArr = Array.from(e.dataTransfer.files); + let importedAny = false; + + for (const file of filesArr) { + if (file.name.toLowerCase().endsWith('.md')) { + try { + const content = await file.text(); + const targetPath = `${workspacePath}/${file.name}`; + await invoke('write_note', { path: targetPath, content }); + importedAny = true; + } catch (err) { + console.error('Failed to import dropped file:', err); + } + } + } + + if (importedAny) { + await refreshWorkspace(); + } + } + }; + return (
+ {/* Full Window Drag Overlay */} + {isDraggingFiles && ( +
+
+

Drop files here

+
+
+ )} + {/* Main Content Area */}
{/* */} diff --git a/src/components/layout/explorer/FileExplorer.tsx b/src/components/layout/explorer/FileExplorer.tsx index fad1681..b810eb3 100644 --- a/src/components/layout/explorer/FileExplorer.tsx +++ b/src/components/layout/explorer/FileExplorer.tsx @@ -36,8 +36,6 @@ export function FileExplorer() { const { files, createFile, - moveNode, - workspacePath, openFileInNewTab, selectFile, setRenamingFileId, @@ -50,10 +48,12 @@ export function FileExplorer() { closeOtherFiles, closeAllFiles, findFile, + moveNode, } = useAppStore(); const [searchQuery, setSearchQuery] = useState(''); // Extract folder name from workspace path + const workspacePath = useAppStore((state) => state.workspacePath); const workspaceName = workspacePath ? workspacePath.split('/').pop() || workspacePath.split('\\').pop() || @@ -65,15 +65,44 @@ export function FileExplorer() { return filterNodes(files, searchQuery.trim()); }, [files, searchQuery]); + const handleDragEnter = (e: React.DragEvent) => { + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) return; + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDragLeave = (e: React.DragEvent) => { + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) return; + e.preventDefault(); + e.stopPropagation(); + }; + const handleDragOver = (e: React.DragEvent) => { + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) return; + e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; }; const handleDrop = (e: React.DragEvent) => { + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) return; + e.preventDefault(); e.stopPropagation(); + const sourceId = e.dataTransfer.getData('text/plain'); if (sourceId) { moveNode(sourceId, 'root', 'root'); @@ -82,8 +111,12 @@ export function FileExplorer() { return (
{/* Header: Workspace Folder Name (Matches Tab Height) */}
{ // Only show the explorer menu if right-clicking the empty area // (not on a file/folder item, which handles its own context menu) diff --git a/src/components/layout/explorer/FileTreeItem.tsx b/src/components/layout/explorer/FileTreeItem.tsx index a5ca97c..51e6e5e 100644 --- a/src/components/layout/explorer/FileTreeItem.tsx +++ b/src/components/layout/explorer/FileTreeItem.tsx @@ -112,6 +112,13 @@ export function FileTreeItem({ node, depth = 0 }: FileTreeItemProps) { const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) { + return; // Bubble up to let FileExplorer handle external files + } + e.stopPropagation(); e.dataTransfer.dropEffect = 'move'; @@ -160,6 +167,11 @@ export function FileTreeItem({ node, depth = 0 }: FileTreeItemProps) { const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) return; + e.stopPropagation(); setDragState((prev) => ({ ...prev, isOver: false })); @@ -171,6 +183,11 @@ export function FileTreeItem({ node, depth = 0 }: FileTreeItemProps) { const handleDrop = (e: React.DragEvent) => { e.preventDefault(); + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) return; // Bubble to FileExplorer + e.stopPropagation(); setDragState((prev) => ({ ...prev, isOver: false })); diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index af8c898..594dc41 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -30,6 +30,10 @@ interface AppState { searchQuery: string; searchResults: SearchResult[]; + // Drag and Drop State + isDraggingFiles: boolean; + setDraggingFiles: (isDragging: boolean) => void; + // Workspace Actions setWorkspacePath: (path: string | null) => void; setFiles: (files: FileNode[]) => void; @@ -100,6 +104,10 @@ export const useAppStore = create((set, get) => ({ searchQuery: '', searchResults: [], + isDraggingFiles: false, + setDraggingFiles: (isDragging: boolean) => + set({ isDraggingFiles: isDragging }), + // Workspace actions setWorkspacePath: (path: string | null) => set({ workspacePath: path }),