diff options
| author | haoyuren <13851610112@163.com> | 2026-03-13 16:48:56 -0500 |
|---|---|---|
| committer | haoyuren <13851610112@163.com> | 2026-03-13 16:48:56 -0500 |
| commit | c309944494eb2de63bf9b35ea722d50b52e688a3 (patch) | |
| tree | 043d7a4cbacfa6416a5f6215146ab83f64157ae9 /src/renderer | |
| parent | eb0c0f1a9fc06d05ea0ecaa9361e0beb49eee68e (diff) | |
Add drag-and-drop file upload, fix project creation modal and API endpoints
- Add file upload to Overleaf projects via multipart POST with correct
"name" text field (server reads filename from req.body.name, not from
Content-Disposition filename)
- Add drag-and-drop support in file tree panel with visual feedback
- Replace window.prompt() with custom modal for new project creation
(prompt() returns null in Electron)
- Fix API endpoints: /api/project/new → /project/new
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'src/renderer')
| -rw-r--r-- | src/renderer/src/App.css | 5 | ||||
| -rw-r--r-- | src/renderer/src/App.tsx | 11 | ||||
| -rw-r--r-- | src/renderer/src/components/FileTree.tsx | 52 | ||||
| -rw-r--r-- | src/renderer/src/components/ProjectList.tsx | 37 |
4 files changed, 99 insertions, 6 deletions
diff --git a/src/renderer/src/App.css b/src/renderer/src/App.css index 63dc8bf..c6d8126 100644 --- a/src/renderer/src/App.css +++ b/src/renderer/src/App.css @@ -303,6 +303,11 @@ html, body, #root { background: var(--bg-secondary); user-select: none; } +.file-tree-dragover { + outline: 2px dashed #4ECDA0; + outline-offset: -2px; + background: rgba(78, 205, 160, 0.08); +} .file-tree-header { display: flex; diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index df9e251..869bdae 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -53,6 +53,17 @@ export default function App() { const [checkingSession, setCheckingSession] = useState(true) + // Prevent Electron from navigating to dropped files + useEffect(() => { + const prevent = (e: DragEvent) => e.preventDefault() + document.addEventListener('dragover', prevent) + document.addEventListener('drop', prevent) + return () => { + document.removeEventListener('dragover', prevent) + document.removeEventListener('drop', prevent) + } + }, []) + // Check session on startup useEffect(() => { window.api.overleafHasWebSession().then(({ loggedIn }) => { diff --git a/src/renderer/src/components/FileTree.tsx b/src/renderer/src/components/FileTree.tsx index 0dda560..a95b0c3 100644 --- a/src/renderer/src/components/FileTree.tsx +++ b/src/renderer/src/components/FileTree.tsx @@ -263,6 +263,51 @@ export default function FileTree() { closeMenu() } + const [dragOver, setDragOver] = useState(false) + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragOver(true) + }, []) + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragOver(false) + }, []) + + const handleDrop = useCallback(async (e: React.DragEvent) => { + e.preventDefault() + e.stopPropagation() + setDragOver(false) + + const projectId = useAppStore.getState().overleafProjectId + const folderId = useAppStore.getState().rootFolderId + if (!projectId || !folderId) return + + const droppedFiles = e.dataTransfer.files + if (droppedFiles.length === 0) return + + for (let i = 0; i < droppedFiles.length; i++) { + const file = droppedFiles[i] + const filePath = window.api.getPathForFile(file) + const fileName = file.name + + useAppStore.getState().setStatusMessage(`Uploading ${fileName}...`) + const result = await window.api.uploadFileToProject(projectId, folderId, filePath, fileName) + if (result.success) { + useAppStore.getState().setStatusMessage(`Uploaded ${fileName}`) + } else { + useAppStore.getState().setStatusMessage(`Upload failed: ${result.message}`) + return + } + } + + // Refresh file tree + await reconnectProject(projectId) + }, []) + const handleOpenInOverleaf = () => { const projectId = useAppStore.getState().overleafProjectId if (projectId) { @@ -272,7 +317,12 @@ export default function FileTree() { } return ( - <div className="file-tree"> + <div + className={`file-tree${dragOver ? ' file-tree-dragover' : ''}`} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + > <div className="file-tree-header"> <span>FILES</span> </div> diff --git a/src/renderer/src/components/ProjectList.tsx b/src/renderer/src/components/ProjectList.tsx index a3c9c37..58a6bb0 100644 --- a/src/renderer/src/components/ProjectList.tsx +++ b/src/renderer/src/components/ProjectList.tsx @@ -30,6 +30,8 @@ export default function ProjectList({ onOpenProject }: Props) { const [busyText, setBusyText] = useState('') const [sortBy, setSortBy] = useState<SortKey>('lastUpdated') const [sortOrder, setSortOrder] = useState<SortOrder>('desc') + const [showNewProject, setShowNewProject] = useState(false) + const [newProjectName, setNewProjectName] = useState('Untitled Project') const { setStatusMessage } = useAppStore() const loadProjects = useCallback(async () => { @@ -76,18 +78,20 @@ export default function ProjectList({ onOpenProject }: Props) { } const handleCreateProject = async () => { - const name = prompt('Project name:', 'Untitled Project') - if (!name?.trim()) return + const name = newProjectName.trim() + if (!name) return + setShowNewProject(false) setError('') setBusy(true) setBusyText('Creating project...') - const result = await window.api.overleafCreateProject(name.trim()) + const result = await window.api.overleafCreateProject(name) setBusy(false) if (result.success && result.projectId) { - setStatusMessage(`Created "${name.trim()}"`) + setStatusMessage(`Created "${name}"`) + setNewProjectName('Untitled Project') loadProjects() } else { setError(result.message || 'Failed to create project') @@ -225,7 +229,7 @@ export default function ProjectList({ onOpenProject }: Props) { placeholder="Search projects..." autoFocus /> - <button className="btn btn-primary btn-sm" onClick={handleCreateProject}> + <button className="btn btn-primary btn-sm" onClick={() => setShowNewProject(true)}> New Project </button> <button className="btn btn-secondary btn-sm" onClick={handleUploadProject}> @@ -284,6 +288,29 @@ export default function ProjectList({ onOpenProject }: Props) { </> )} </div> + {showNewProject && ( + <div className="modal-overlay" onClick={() => setShowNewProject(false)}> + <div className="modal-box" onClick={(e) => e.stopPropagation()}> + <h3 style={{ margin: '0 0 12px' }}>New Project</h3> + <input + type="text" + className="projects-search" + value={newProjectName} + onChange={(e) => setNewProjectName(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleCreateProject() + if (e.key === 'Escape') setShowNewProject(false) + }} + autoFocus + style={{ width: '100%', marginBottom: 12 }} + /> + <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}> + <button className="btn btn-secondary btn-sm" onClick={() => setShowNewProject(false)}>Cancel</button> + <button className="btn btn-primary btn-sm" onClick={handleCreateProject}>Create</button> + </div> + </div> + </div> + )} </div> ) } |
