From 00b6b936bff1bebcb9a95c205dc78a17abfa4f29 Mon Sep 17 00:00:00 2001 From: Anik <160725845+coderanik@users.noreply.github.com> Date: Thu, 12 Mar 2026 00:32:11 +0530 Subject: [PATCH 1/5] feat: handle drag and drop of external .md files --- .../layout/explorer/FileExplorer.tsx | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/components/layout/explorer/FileExplorer.tsx b/src/components/layout/explorer/FileExplorer.tsx index fad1681..edb3252 100644 --- a/src/components/layout/explorer/FileExplorer.tsx +++ b/src/components/layout/explorer/FileExplorer.tsx @@ -1,5 +1,7 @@ import { useState, useMemo } from 'react'; +import { invoke } from '@tauri-apps/api/core'; import { useAppStore } from '../../../store/useAppStore'; +import { useWorkspace } from '../../../hooks/useWorkspace'; import type { FileNode } from '../../../types/fileSystem'; import { FileTreeItem } from './FileTreeItem'; import { showExplorerContextMenu } from '../../../util/contextMenu'; @@ -51,6 +53,7 @@ export function FileExplorer() { closeAllFiles, findFile, } = useAppStore(); + const { refreshWorkspace } = useWorkspace(); const [searchQuery, setSearchQuery] = useState(''); // Extract folder name from workspace path @@ -68,12 +71,42 @@ export function FileExplorer() { const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - e.dataTransfer.dropEffect = 'move'; + if (e.dataTransfer.types.includes('Files')) { + e.dataTransfer.dropEffect = 'copy'; + } else { + e.dataTransfer.dropEffect = 'move'; + } }; - const handleDrop = (e: React.DragEvent) => { + const handleDrop = async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); + + if (e.dataTransfer.types.includes('Files') && 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; + } + const sourceId = e.dataTransfer.getData('text/plain'); if (sourceId) { moveNode(sourceId, 'root', 'root'); From c01cf16395e87cdbf09d4db98322a0f556323f2f Mon Sep 17 00:00:00 2001 From: Anik <160725845+coderanik@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:09:51 +0530 Subject: [PATCH 2/5] fix: allow external md files to be dropped over existing tree items --- docs/changelogs/cinder-v0.2.md | 1 - package.json | 2 +- src/components/layout/explorer/FileExplorer.tsx | 10 ++++++++-- src/components/layout/explorer/FileTreeItem.tsx | 17 +++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) 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/explorer/FileExplorer.tsx b/src/components/layout/explorer/FileExplorer.tsx index edb3252..532e001 100644 --- a/src/components/layout/explorer/FileExplorer.tsx +++ b/src/components/layout/explorer/FileExplorer.tsx @@ -71,7 +71,10 @@ export function FileExplorer() { const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - if (e.dataTransfer.types.includes('Files')) { + const hasFiles = + e.dataTransfer.types && + Array.from(e.dataTransfer.types).includes('Files'); + if (hasFiles) { e.dataTransfer.dropEffect = 'copy'; } else { e.dataTransfer.dropEffect = 'move'; @@ -82,7 +85,10 @@ export function FileExplorer() { e.preventDefault(); e.stopPropagation(); - if (e.dataTransfer.types.includes('Files') && e.dataTransfer.files && e.dataTransfer.files.length > 0) { + 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); 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 })); From 2285da1596f7ec78b78e5b593bd4c2fcf67467db Mon Sep 17 00:00:00 2001 From: Anik <160725845+coderanik@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:04:33 +0530 Subject: [PATCH 3/5] fix: enable drag and drop in tauri config --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c8fa2da..ca2c629 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -17,7 +17,7 @@ "height": 800, "resizable": true, "fullscreen": false, - "dragDropEnabled": false + "dragDropEnabled": true } ], "security": { From fcdb6287a1f07687e8d12f7022693d097ea556d4 Mon Sep 17 00:00:00 2001 From: Anik <160725845+coderanik@users.noreply.github.com> Date: Fri, 13 Mar 2026 01:50:43 +0530 Subject: [PATCH 4/5] feat: add full-window drag-and-drop overlay for file imports - Add global isDraggingFiles state to app store - Implement full-window drag overlay in MainLayout - Move drop handling from FileExplorer to MainLayout - Files can now be dropped anywhere in the window and will be imported to explorer - Overlay shows 'Drop files here' message with blur effect - Automatically hides overlay after drop or cancel --- src-tauri/tauri.conf.json | 2 +- src/components/layout/MainLayout.tsx | 84 ++++++++++++++++++- .../layout/explorer/FileExplorer.tsx | 58 ++++--------- src/store/useAppStore.ts | 8 ++ 4 files changed, 109 insertions(+), 43 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ca2c629..c8fa2da 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -17,7 +17,7 @@ "height": 800, "resizable": true, "fullscreen": false, - "dragDropEnabled": true + "dragDropEnabled": false } ], "security": { diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 52d3f50..47da400 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,90 @@ export function MainLayout({ sidebarContent, editorContent }: MainLayoutProps) { setExplorerCollapsed(!isExplorerCollapsed); }; + 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'; + } + }; + + const handleWindowDragLeave = (e: React.DragEvent) => { + // Only hide when leaving the window entirely + if (e.currentTarget === e.target) { + 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 532e001..2f9c8ab 100644 --- a/src/components/layout/explorer/FileExplorer.tsx +++ b/src/components/layout/explorer/FileExplorer.tsx @@ -1,7 +1,5 @@ import { useState, useMemo } from 'react'; -import { invoke } from '@tauri-apps/api/core'; import { useAppStore } from '../../../store/useAppStore'; -import { useWorkspace } from '../../../hooks/useWorkspace'; import type { FileNode } from '../../../types/fileSystem'; import { FileTreeItem } from './FileTreeItem'; import { showExplorerContextMenu } from '../../../util/contextMenu'; @@ -38,8 +36,6 @@ export function FileExplorer() { const { files, createFile, - moveNode, - workspacePath, openFileInNewTab, selectFile, setRenamingFileId, @@ -52,11 +48,12 @@ export function FileExplorer() { closeOtherFiles, closeAllFiles, findFile, + setDraggingFiles, } = useAppStore(); - const { refreshWorkspace } = useWorkspace(); 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() || @@ -68,61 +65,42 @@ export function FileExplorer() { return filterNodes(files, searchQuery.trim()); }, [files, searchQuery]); - const handleDragOver = (e: React.DragEvent) => { + const handleDragEnter = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); const hasFiles = e.dataTransfer.types && Array.from(e.dataTransfer.types).includes('Files'); if (hasFiles) { - e.dataTransfer.dropEffect = 'copy'; - } else { - e.dataTransfer.dropEffect = 'move'; + setDraggingFiles(true); } }; - const handleDrop = async (e: React.DragEvent) => { + const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); + }; + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); 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; - } - - const sourceId = e.dataTransfer.getData('text/plain'); - if (sourceId) { - moveNode(sourceId, 'root', 'root'); + if (hasFiles) { + e.dataTransfer.dropEffect = 'copy'; + } else { + e.dataTransfer.dropEffect = 'move'; } }; 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/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 }), From 7dc67f2e0a5e605824b49fbf1c6c160c467de6f0 Mon Sep 17 00:00:00 2001 From: Anik <160725845+coderanik@users.noreply.github.com> Date: Fri, 13 Mar 2026 22:05:59 +0530 Subject: [PATCH 5/5] feat: configure and fix drag and drop functionality for .md files --- src/components/layout/MainLayout.tsx | 16 ++++++++- .../layout/explorer/FileExplorer.tsx | 36 +++++++++++++------ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 47da400..172f770 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -76,6 +76,16 @@ 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 = @@ -83,12 +93,15 @@ export function MainLayout({ sidebarContent, editorContent }: MainLayoutProps) { 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 === e.target) { + if (!e.currentTarget.contains(e.relatedTarget as Node)) { setDraggingFiles(false); } }; @@ -134,6 +147,7 @@ export function MainLayout({ sidebarContent, editorContent }: MainLayoutProps) { backgroundColor: 'var(--bg-primary)', color: 'var(--text-primary)', }} + onDragEnter={handleWindowDragEnter} onDragOver={handleWindowDragOver} onDragLeave={handleWindowDragLeave} onDrop={handleWindowDrop} diff --git a/src/components/layout/explorer/FileExplorer.tsx b/src/components/layout/explorer/FileExplorer.tsx index 2f9c8ab..b810eb3 100644 --- a/src/components/layout/explorer/FileExplorer.tsx +++ b/src/components/layout/explorer/FileExplorer.tsx @@ -48,7 +48,7 @@ export function FileExplorer() { closeOtherFiles, closeAllFiles, findFile, - setDraggingFiles, + moveNode, } = useAppStore(); const [searchQuery, setSearchQuery] = useState(''); @@ -66,31 +66,46 @@ export function FileExplorer() { }, [files, searchQuery]); const handleDragEnter = (e: React.DragEvent) => { - e.preventDefault(); - e.stopPropagation(); const hasFiles = e.dataTransfer.types && Array.from(e.dataTransfer.types).includes('Files'); - if (hasFiles) { - setDraggingFiles(true); - } + 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) { - e.dataTransfer.dropEffect = 'copy'; - } else { - e.dataTransfer.dropEffect = 'move'; + if (hasFiles) return; + + e.preventDefault(); + e.stopPropagation(); + + const sourceId = e.dataTransfer.getData('text/plain'); + if (sourceId) { + moveNode(sourceId, 'root', 'root'); } }; @@ -101,6 +116,7 @@ export function FileExplorer() { onDragEnter={handleDragEnter} onDragLeave={handleDragLeave} onDragOver={handleDragOver} + onDrop={handleDrop} > {/* Header: Workspace Folder Name (Matches Tab Height) */}