Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/changelogs/cinder-v0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}
}
98 changes: 97 additions & 1 deletion src/components/layout/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,11 +15,15 @@ export function MainLayout({ sidebarContent, editorContent }: MainLayoutProps) {
setExplorerCollapsed,
sidebarWidth,
setSidebarWidth,
isDraggingFiles,
setDraggingFiles,
workspacePath,
} = useAppStore();

const isResizingRef = useRef(false);
const sidebarRef = useRef<HTMLDivElement>(null);
const [isResizing, setIsResizing] = useState(false);
const { refreshWorkspace } = useWorkspace();

const startResizing = useCallback(
(e: React.MouseEvent) => {
Expand Down Expand Up @@ -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 (
<div
className="h-screen w-screen flex flex-col overflow-hidden"
className="h-screen w-screen flex flex-col overflow-hidden relative"
style={{
backgroundColor: 'var(--bg-primary)',
color: 'var(--text-primary)',
}}
onDragEnter={handleWindowDragEnter}
onDragOver={handleWindowDragOver}
onDragLeave={handleWindowDragLeave}
onDrop={handleWindowDrop}
>
{/* Full Window Drag Overlay */}
{isDraggingFiles && (
<div
className="absolute inset-0 z-[9999] flex items-center justify-center pointer-events-none"
style={{
backgroundColor: 'rgba(0, 0, 0, 0.6)',
backdropFilter: 'blur(6px)',
}}
>
<div
className="px-12 py-8 rounded-xl border-2 border-dashed"
style={{
borderColor: 'var(--accent-primary)',
backgroundColor: 'var(--bg-secondary)',
color: 'var(--text-primary)',
}}
>
<p className="text-xl font-medium">Drop files here</p>
</div>
</div>
)}

{/* Main Content Area */}
<div className="flex-1 flex min-h-0 relative">
{/* <ActivityBar /> */}
Expand Down
41 changes: 36 additions & 5 deletions src/components/layout/explorer/FileExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ export function FileExplorer() {
const {
files,
createFile,
moveNode,
workspacePath,
openFileInNewTab,
selectFile,
setRenamingFileId,
Expand All @@ -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() ||
Expand All @@ -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');
Expand All @@ -82,8 +111,12 @@ export function FileExplorer() {

return (
<div
className="h-full flex flex-col"
className="h-full flex flex-col relative"
style={{ backgroundColor: 'var(--bg-primary)' }}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
{/* Header: Workspace Folder Name (Matches Tab Height) */}
<div
Expand Down Expand Up @@ -132,8 +165,6 @@ export function FileExplorer() {
{/* Main File List */}
<div
className="flex-1 overflow-y-auto no-scrollbar pt-0 px-2 pb-2"
onDragOver={handleDragOver}
onDrop={handleDrop}
onContextMenu={(e) => {
// Only show the explorer menu if right-clicking the empty area
// (not on a file/folder item, which handles its own context menu)
Expand Down
17 changes: 17 additions & 0 deletions src/components/layout/explorer/FileTreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 }));

Expand All @@ -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 }));

Expand Down
8 changes: 8 additions & 0 deletions src/store/useAppStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -100,6 +104,10 @@ export const useAppStore = create<AppState>((set, get) => ({
searchQuery: '',
searchResults: [],

isDraggingFiles: false,
setDraggingFiles: (isDragging: boolean) =>
set({ isDraggingFiles: isDragging }),

// Workspace actions
setWorkspacePath: (path: string | null) => set({ workspacePath: path }),

Expand Down
Loading