diff options
Diffstat (limited to 'src/renderer')
| -rw-r--r-- | src/renderer/src/App.css | 55 | ||||
| -rw-r--r-- | src/renderer/src/App.tsx | 30 | ||||
| -rw-r--r-- | src/renderer/src/components/Editor.tsx | 3 | ||||
| -rw-r--r-- | src/renderer/src/components/PdfViewer.tsx | 38 | ||||
| -rw-r--r-- | src/renderer/src/components/ReviewPanel.tsx | 12 | ||||
| -rw-r--r-- | src/renderer/src/components/Toolbar.tsx | 57 | ||||
| -rw-r--r-- | src/renderer/src/stores/appStore.ts | 8 |
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, |
