summaryrefslogtreecommitdiff
path: root/src/renderer
diff options
context:
space:
mode:
Diffstat (limited to 'src/renderer')
-rw-r--r--src/renderer/src/App.css55
-rw-r--r--src/renderer/src/App.tsx30
-rw-r--r--src/renderer/src/components/Editor.tsx3
-rw-r--r--src/renderer/src/components/PdfViewer.tsx38
-rw-r--r--src/renderer/src/components/ReviewPanel.tsx12
-rw-r--r--src/renderer/src/components/Toolbar.tsx57
-rw-r--r--src/renderer/src/stores/appStore.ts8
7 files changed, 172 insertions, 31 deletions
diff --git a/src/renderer/src/App.css b/src/renderer/src/App.css
index a6a4003..4c3f731 100644
--- a/src/renderer/src/App.css
+++ b/src/renderer/src/App.css
@@ -263,6 +263,61 @@ html, body, #root {
50% { opacity: 0.6; }
}
+.compile-btn-group {
+ display: flex;
+ position: relative;
+}
+
+.compile-btn-group .toolbar-btn-primary:first-child {
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
+ border-right: 1px solid rgba(255,255,255,0.2);
+}
+
+.compile-dropdown-toggle {
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0 !important;
+ padding: 0 4px !important;
+ min-width: 20px;
+ font-size: 9px;
+}
+
+.compile-dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ margin-top: 4px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ z-index: 100;
+ min-width: 160px;
+ overflow: hidden;
+}
+
+.compile-dropdown-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 6px 12px;
+ border: none;
+ background: none;
+ color: var(--text-primary);
+ font-size: 12px;
+ cursor: pointer;
+ text-align: left;
+}
+
+.compile-dropdown-item:hover {
+ background: var(--bg-hover);
+}
+
+.compile-dropdown-hint {
+ font-size: 10px;
+ color: var(--text-muted);
+ margin-left: 12px;
+}
+
.toolbar-main-doc {
font-size: 11px;
color: var(--text-muted);
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index ca2fc1e..6992a17 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -219,15 +219,37 @@ export default function App() {
setStatusMessage('No main document set')
return
}
+ state.setCompiling(true)
+ state.clearCompileLog()
+ setStatusMessage('Compiling on server...')
+
+ const result = await window.api.overleafServerCompile(mainDoc)
+
+ if (result.log) {
+ useAppStore.getState().appendCompileLog(result.log)
+ }
+ if (result.pdfPath) {
+ useAppStore.getState().setPdfPath(result.pdfPath)
+ }
+ useAppStore.getState().setCompiling(false)
+ setStatusMessage(result.success ? 'Compiled successfully' : 'Compilation had errors — check Log tab')
+ }
+
+ const handleLocalCompile = async () => {
+ const state = useAppStore.getState()
+ const mainDoc = state.mainDocument || state.overleafProject?.rootDocId
+ if (!mainDoc) {
+ setStatusMessage('No main document set')
+ return
+ }
const relPath = state.docPathMap[mainDoc] || mainDoc
state.setCompiling(true)
state.clearCompileLog()
- setStatusMessage('Compiling...')
+ setStatusMessage('Compiling locally...')
const result = await window.api.overleafSocketCompile(relPath)
- const storeLog = useAppStore.getState().compileLog
- if (!storeLog && result.log) {
+ if (!useAppStore.getState().compileLog && result.log) {
useAppStore.getState().appendCompileLog(result.log)
}
if (result.pdfPath) {
@@ -358,7 +380,7 @@ export default function App() {
<ErrorBoundary>
<ModalProvider />
<div className="app">
- <Toolbar onCompile={handleCompile} onBack={handleBackToProjects} />
+ <Toolbar onCompile={handleCompile} onLocalCompile={handleLocalCompile} onBack={handleBackToProjects} />
<div className="main-content">
<PanelGroup direction="horizontal">
{showFileTree && (
diff --git a/src/renderer/src/components/Editor.tsx b/src/renderer/src/components/Editor.tsx
index 4252464..4edb945 100644
--- a/src/renderer/src/components/Editor.tsx
+++ b/src/renderer/src/components/Editor.tsx
@@ -335,8 +335,9 @@ export default function Editor() {
}, [activeTab, pathDocMap])
// Sync comment ranges to CodeMirror (exclude resolved threads)
+ // Skip until resolvedThreadIds has been loaded (non-null) to avoid flashing resolved highlights
useEffect(() => {
- if (!viewRef.current || !activeTab) return
+ if (!viewRef.current || !activeTab || resolvedThreadIds === null) return
const ranges: CommentRange[] = []
for (const [threadId, ctx] of Object.entries(commentContexts)) {
if (ctx.file === activeTab && ctx.text && !resolvedThreadIds.has(threadId)) {
diff --git a/src/renderer/src/components/PdfViewer.tsx b/src/renderer/src/components/PdfViewer.tsx
index 1e1fd7c..c5fe8c4 100644
--- a/src/renderer/src/components/PdfViewer.tsx
+++ b/src/renderer/src/components/PdfViewer.tsx
@@ -241,13 +241,32 @@ export default function PdfViewer() {
const result = await window.api.synctexEdit(pdfPath, pageNum, pdfX, pdfY)
if (!result) return
- // Navigate to the source file:line
- try {
- const content = await window.api.readFile(result.file)
- useAppStore.getState().setFileContent(result.file, content)
- useAppStore.getState().openFile(result.file, result.file.split('/').pop() || result.file)
- useAppStore.getState().setPendingGoTo({ file: result.file, line: result.line })
- } catch { /* file not found */ }
+ // Navigate to source — synctex returns relative path (e.g. "latex/main.tex")
+ const store = useAppStore.getState()
+ const relPath = result.file
+
+ // If already loaded in editor, just navigate
+ if (store.fileContents[relPath]) {
+ store.openFile(relPath, relPath.split('/').pop() || relPath)
+ store.setPendingGoTo({ file: relPath, line: result.line })
+ return
+ }
+
+ // Not loaded — join via socket
+ const docId = store.pathDocMap[relPath]
+ if (docId) {
+ try {
+ const joinResult = await window.api.otJoinDoc(docId)
+ if (joinResult.success && joinResult.content !== undefined) {
+ useAppStore.getState().setFileContent(relPath, joinResult.content)
+ if (joinResult.version !== undefined) {
+ useAppStore.getState().setDocVersion(docId, joinResult.version)
+ }
+ useAppStore.getState().openFile(relPath, relPath.split('/').pop() || relPath)
+ useAppStore.getState().setPendingGoTo({ file: relPath, line: result.line })
+ }
+ } catch { /* failed to join doc */ }
+ }
}, [pdfPath])
// Render PDF (with lock to prevent double-render)
@@ -356,6 +375,11 @@ export default function PdfViewer() {
<span className="pdf-scale">{Math.round(scale * 100)}%</span>
<button className="toolbar-btn" onClick={() => setScale((s) => Math.min(3, s + 0.25))}>+</button>
<button className="toolbar-btn" onClick={() => setScale(1.0)}>Fit</button>
+ {pdfPath && (
+ <button className="toolbar-btn" onClick={() => window.api.savePdf(pdfPath)} title="Download PDF">
+ ↓
+ </button>
+ )}
</>
)}
{tab === 'log' && (
diff --git a/src/renderer/src/components/ReviewPanel.tsx b/src/renderer/src/components/ReviewPanel.tsx
index 7e6c9e5..9396ddf 100644
--- a/src/renderer/src/components/ReviewPanel.tsx
+++ b/src/renderer/src/components/ReviewPanel.tsx
@@ -123,7 +123,7 @@ export default function ReviewPanel() {
}
})
const store = useAppStore.getState()
- store.setResolvedThreadIds(new Set([...store.resolvedThreadIds, threadId]))
+ store.setResolvedThreadIds(new Set([...(store.resolvedThreadIds || []), threadId]))
break
}
case 'reopen-thread': {
@@ -137,7 +137,7 @@ export default function ReviewPanel() {
return { ...prev, [threadId]: t }
})
const store = useAppStore.getState()
- const ids = new Set(store.resolvedThreadIds)
+ const ids = new Set(store.resolvedThreadIds || [])
ids.delete(threadId)
store.setResolvedThreadIds(ids)
break
@@ -154,7 +154,7 @@ export default function ReviewPanel() {
const newCtx = { ...store.commentContexts }
delete newCtx[threadId]
store.setCommentContexts(newCtx)
- const ids = new Set(store.resolvedThreadIds)
+ const ids = new Set(store.resolvedThreadIds || [])
ids.delete(threadId)
store.setResolvedThreadIds(ids)
break
@@ -228,7 +228,7 @@ export default function ReviewPanel() {
return { ...prev, [threadId]: { ...prev[threadId], resolved: true, resolved_at: new Date().toISOString() } }
})
const store = useAppStore.getState()
- store.setResolvedThreadIds(new Set([...store.resolvedThreadIds, threadId]))
+ store.setResolvedThreadIds(new Set([...(store.resolvedThreadIds || []), threadId]))
await window.api.overleafResolveThread(overleafProjectId, threadId, getDocIdForThread(threadId))
}
@@ -244,7 +244,7 @@ export default function ReviewPanel() {
return { ...prev, [threadId]: t }
})
const store = useAppStore.getState()
- const ids = new Set(store.resolvedThreadIds)
+ const ids = new Set(store.resolvedThreadIds || [])
ids.delete(threadId)
store.setResolvedThreadIds(ids)
await window.api.overleafReopenThread(overleafProjectId, threadId, getDocIdForThread(threadId))
@@ -307,7 +307,7 @@ export default function ReviewPanel() {
const newCtx = { ...store.commentContexts }
delete newCtx[threadId]
store.setCommentContexts(newCtx)
- const ids = new Set(store.resolvedThreadIds)
+ const ids = new Set(store.resolvedThreadIds || [])
ids.delete(threadId)
store.setResolvedThreadIds(ids)
diff --git a/src/renderer/src/components/Toolbar.tsx b/src/renderer/src/components/Toolbar.tsx
index b480ede..94ddb5e 100644
--- a/src/renderer/src/components/Toolbar.tsx
+++ b/src/renderer/src/components/Toolbar.tsx
@@ -1,20 +1,37 @@
// Copyright (c) 2026 Yuren Hao
// Licensed under AGPL-3.0 - see LICENSE file
+import { useState, useRef, useEffect } from 'react'
import { useAppStore } from '../stores/appStore'
interface ToolbarProps {
onCompile: () => void
+ onLocalCompile: () => void
onBack: () => void
}
-export default function Toolbar({ onCompile, onBack }: ToolbarProps) {
+export default function Toolbar({ onCompile, onLocalCompile, onBack }: ToolbarProps) {
const {
compiling, toggleTerminal, toggleFileTree, showTerminal, showFileTree,
showReviewPanel, toggleReviewPanel, showChat, toggleChat,
connectionState, overleafProject, onlineUsersCount
} = useAppStore()
+ const [showCompileMenu, setShowCompileMenu] = useState(false)
+ const menuRef = useRef<HTMLDivElement>(null)
+
+ // Close menu on outside click
+ useEffect(() => {
+ if (!showCompileMenu) return
+ const handler = (e: MouseEvent) => {
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
+ setShowCompileMenu(false)
+ }
+ }
+ document.addEventListener('mousedown', handler)
+ return () => document.removeEventListener('mousedown', handler)
+ }, [showCompileMenu])
+
const projectName = overleafProject?.name || 'Project'
const connectionDot = connectionState === 'connected' ? 'connection-dot-green'
@@ -37,14 +54,36 @@ export default function Toolbar({ onCompile, onBack }: ToolbarProps) {
</span>
</div>
<div className="toolbar-center">
- <button
- className={`toolbar-btn toolbar-btn-primary ${compiling ? 'compiling' : ''}`}
- onClick={onCompile}
- disabled={compiling}
- title="Compile (Cmd+B)"
- >
- {compiling ? 'Compiling...' : 'Compile'}
- </button>
+ <div className="compile-btn-group" ref={menuRef}>
+ <button
+ className={`toolbar-btn toolbar-btn-primary ${compiling ? 'compiling' : ''}`}
+ onClick={onCompile}
+ disabled={compiling}
+ title="Compile on Overleaf server (Cmd+B)"
+ >
+ {compiling ? 'Compiling...' : 'Compile'}
+ </button>
+ <button
+ className="toolbar-btn toolbar-btn-primary compile-dropdown-toggle"
+ onClick={() => setShowCompileMenu(!showCompileMenu)}
+ disabled={compiling}
+ title="Compile options"
+ >
+ ▾
+ </button>
+ {showCompileMenu && (
+ <div className="compile-dropdown-menu">
+ <button className="compile-dropdown-item" onClick={() => { setShowCompileMenu(false); onCompile() }}>
+ Server Compile
+ <span className="compile-dropdown-hint">Cmd+B</span>
+ </button>
+ <button className="compile-dropdown-item" onClick={() => { setShowCompileMenu(false); onLocalCompile() }}>
+ Local Compile
+ <span className="compile-dropdown-hint">latexmk</span>
+ </button>
+ </div>
+ )}
+ </div>
</div>
<div className="toolbar-right">
{onlineUsersCount > 0 && (
diff --git a/src/renderer/src/stores/appStore.ts b/src/renderer/src/stores/appStore.ts
index ff56c28..9a1d441 100644
--- a/src/renderer/src/stores/appStore.ts
+++ b/src/renderer/src/stores/appStore.ts
@@ -109,8 +109,8 @@ interface AppState {
// Comment data
commentContexts: Record<string, CommentContext>
setCommentContexts: (c: Record<string, CommentContext>) => void
- resolvedThreadIds: Set<string>
- setResolvedThreadIds: (ids: Set<string>) => void
+ resolvedThreadIds: Set<string> | null
+ setResolvedThreadIds: (ids: Set<string> | null) => void
overleafDocs: Record<string, string>
setOverleafDocs: (d: Record<string, string>) => void
hoveredThreadId: string | null
@@ -221,7 +221,7 @@ export const useAppStore = create<AppState>((set) => ({
commentContexts: {},
setCommentContexts: (c) => set({ commentContexts: c }),
- resolvedThreadIds: new Set<string>(),
+ resolvedThreadIds: null,
setResolvedThreadIds: (ids) => set({ resolvedThreadIds: ids }),
overleafDocs: {},
setOverleafDocs: (d) => set({ overleafDocs: d }),
@@ -255,7 +255,7 @@ export const useAppStore = create<AppState>((set) => ({
rootFolderId: '',
syncDir: '',
commentContexts: {},
- resolvedThreadIds: new Set<string>(),
+ resolvedThreadIds: null,
overleafDocs: {},
hoveredThreadId: null,
focusedThreadId: null,