summaryrefslogtreecommitdiff
path: root/src/renderer
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-13 16:48:56 -0500
committerhaoyuren <13851610112@163.com>2026-03-13 16:48:56 -0500
commitc309944494eb2de63bf9b35ea722d50b52e688a3 (patch)
tree043d7a4cbacfa6416a5f6215146ab83f64157ae9 /src/renderer
parenteb0c0f1a9fc06d05ea0ecaa9361e0beb49eee68e (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.css5
-rw-r--r--src/renderer/src/App.tsx11
-rw-r--r--src/renderer/src/components/FileTree.tsx52
-rw-r--r--src/renderer/src/components/ProjectList.tsx37
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>
)
}