diff options
| -rw-r--r-- | README.md | 50 | ||||
| -rw-r--r-- | electron-builder.yml | 20 | ||||
| -rw-r--r-- | resources/icon.icns | bin | 0 -> 168312 bytes | |||
| -rw-r--r-- | resources/logo.svg | 35 | ||||
| -rw-r--r-- | src/main/index.ts | 28 | ||||
| -rw-r--r-- | src/renderer/src/App.css | 2 | ||||
| -rw-r--r-- | src/renderer/src/App.tsx | 21 |
7 files changed, 108 insertions, 48 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..5bdaaa2 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# LatteX + +<p align="center"> + <img src="resources/logo.svg" width="128" height="128" alt="LatteX logo"> +</p> + +<p align="center"> + LaTeX editor with real-time Overleaf sync, themed in Cosmic Latte. +</p> + +## Features + +- **Real-time Overleaf sync** — WebSocket-based OT collaboration, live co-editing +- **Bidirectional file sync** — edit `.tex` files on disk (e.g. with Claude Code in the integrated terminal) and changes sync to Overleaf automatically +- **Local LaTeX compilation** — compile PDFs locally with `latexmk`, no Overleaf compile limits +- **PDF viewer** — built-in viewer with SyncTeX forward/inverse search, pinch-to-zoom +- **LaTeX autocomplete** — commands, environments, `\ref`, `\cite`, file paths +- **Comments & review** — inline comment highlights with review panel +- **Collaborator cursors** — see other editors' positions in real-time +- **Project chat** — real-time chat panel +- **Integrated terminal** — built-in terminal for CLI tools + +## Install + +Download the latest `.dmg` from [Releases](https://github.com/YurenHao0426/lattex/releases). + +> **Note:** This is an unsigned build. On first launch, right-click → Open, or allow it in System Settings → Privacy & Security. + +### Requirements + +- macOS (Apple Silicon) +- [TeX Live](https://www.tug.org/texlive/) or [MacTeX](https://www.tug.org/mactex/) for local compilation + +## Development + +```bash +npm install +npm run dev +``` + +### Build + +```bash +npm run build +npx electron-builder --mac dmg +``` + +## License + +[AGPL-3.0](LICENSE) diff --git a/electron-builder.yml b/electron-builder.yml index 9152755..5d1d3b4 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -6,7 +6,27 @@ directories: files: - out/**/* - "!node_modules/**/*" + - node_modules/node-pty/**/* + - node_modules/ws/**/* + - node_modules/chokidar/**/* + - node_modules/diff-match-patch/**/* + - node_modules/anymatch/**/* + - node_modules/braces/**/* + - node_modules/fill-range/**/* + - node_modules/glob-parent/**/* + - node_modules/is-binary-path/**/* + - node_modules/is-extglob/**/* + - node_modules/is-glob/**/* + - node_modules/is-number/**/* + - node_modules/normalize-path/**/* + - node_modules/picomatch/**/* + - node_modules/readdirp/**/* + - node_modules/to-regex-range/**/* + - node_modules/binary-extensions/**/* +asarUnpack: + - node_modules/node-pty/**/* mac: + icon: resources/icon.icns target: - dmg category: public.app-category.productivity diff --git a/resources/icon.icns b/resources/icon.icns Binary files differnew file mode 100644 index 0000000..cf000bc --- /dev/null +++ b/resources/icon.icns diff --git a/resources/logo.svg b/resources/logo.svg index bd5af43..017bf65 100644 --- a/resources/logo.svg +++ b/resources/logo.svg @@ -1,28 +1,15 @@ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> - <defs> - <linearGradient id="cupGrad" x1="0" y1="0" x2="0" y2="1"> - <stop offset="0%" stop-color="#FFF8E7"/> - <stop offset="100%" stop-color="#EDE5CE"/> - </linearGradient> - <linearGradient id="coffeeGrad" x1="0" y1="0" x2="0" y2="1"> - <stop offset="0%" stop-color="#A0782C"/> - <stop offset="100%" stop-color="#7A5A1E"/> - </linearGradient> - </defs> - <!-- Background circle --> - <circle cx="256" cy="256" r="240" fill="#6B5B3E"/> + <rect width="512" height="512" rx="80" fill="#6B5B3E"/> <!-- Cup body --> - <path d="M130 190 Q124 390 185 405 L327 405 Q388 390 382 190 Z" fill="url(#cupGrad)" stroke="#D6CEBC" stroke-width="2"/> - <!-- Coffee surface --> - <ellipse cx="256" cy="190" rx="126" ry="36" fill="url(#coffeeGrad)"/> - <!-- Latte art - heart --> - <path d="M256 175 Q240 165 232 172 Q224 180 236 192 L256 208 L276 192 Q288 180 280 172 Q272 165 256 175Z" fill="#D4B880" opacity="0.6"/> - <!-- Cup rim highlight --> - <ellipse cx="256" cy="190" rx="126" ry="36" fill="none" stroke="#D6CEBC" stroke-width="3"/> + <path d="M148 195 Q142 375 195 395 L317 395 Q370 375 364 195 Z" fill="#FFF8E7" stroke="#EDE5CE" stroke-width="2"/> <!-- Handle --> - <path d="M382 230 Q435 235 438 300 Q440 365 390 370" fill="none" stroke="#FFF8E7" stroke-width="12" stroke-linecap="round"/> - <!-- Steam lines --> - <path d="M216 148 Q210 118 220 95" fill="none" stroke="#FFF8E7" stroke-width="5" stroke-linecap="round" opacity="0.5"/> - <path d="M256 140 Q250 105 260 80" fill="none" stroke="#FFF8E7" stroke-width="5" stroke-linecap="round" opacity="0.45"/> - <path d="M296 148 Q290 118 300 95" fill="none" stroke="#FFF8E7" stroke-width="5" stroke-linecap="round" opacity="0.4"/> + <path d="M364 235 Q410 240 412 305 Q414 365 370 370" fill="none" stroke="#FFF8E7" stroke-width="14" stroke-linecap="round"/> + <!-- Liquid surface --> + <ellipse cx="256" cy="195" rx="108" ry="30" fill="#4ECDA0"/> + <!-- Cup rim --> + <ellipse cx="256" cy="195" rx="108" ry="30" fill="none" stroke="#EDE5CE" stroke-width="3"/> + <!-- Cosmic sparkles --> + <path d="M218 128 L224 108 L230 128 L250 134 L230 140 L224 160 L218 140 L198 134 Z" fill="#4ECDA0" opacity="0.9"/> + <path d="M268 100 L273 84 L278 100 L294 105 L278 110 L273 126 L268 110 L252 105 Z" fill="#4ECDA0" opacity="0.7"/> + <path d="M308 118 L313 102 L318 118 L334 123 L318 128 L313 144 L308 128 L292 123 Z" fill="#4ECDA0" opacity="0.55"/> </svg> diff --git a/src/main/index.ts b/src/main/index.ts index b543b46..7fab07a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -38,6 +38,13 @@ function createWindow(): void { } } +/** Safely send IPC to renderer — no-op if window is gone */ +function sendToRenderer(channel: string, ...args: unknown[]) { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(channel, ...args) + } +} + ipcMain.handle('fs:readFile', async (_e, filePath: string) => { return readFile(filePath, 'utf-8') }) @@ -99,11 +106,11 @@ ipcMain.handle('pty:spawn', async (_e, cwd: string) => { }) ptyInstance.onData((data) => { - mainWindow?.webContents.send('pty:data', data) + sendToRenderer('pty:data', data) }) ptyInstance.onExit(() => { - mainWindow?.webContents.send('pty:exit') + sendToRenderer('pty:exit') }) }) @@ -602,7 +609,7 @@ ipcMain.handle('ot:connect', async (_e, projectId: string) => { // Relay events to renderer overleafSock.on('connectionState', (state: string) => { - mainWindow?.webContents.send('ot:connectionState', state) + sendToRenderer('ot:connectionState', state) }) // otUpdateApplied: server acknowledges our op (ack signal for OT client) @@ -610,13 +617,13 @@ ipcMain.handle('ot:connect', async (_e, projectId: string) => { if (name === 'otUpdateApplied') { const update = args[0] as { doc?: string; v?: number } | undefined if (update?.doc) { - mainWindow?.webContents.send('ot:ack', { docId: update.doc }) + sendToRenderer('ot:ack', { docId: update.doc }) } } }) overleafSock.on('docRejoined', (docId: string, result: JoinDocResult) => { - mainWindow?.webContents.send('ot:docRejoined', { + sendToRenderer('ot:docRejoined', { docId, content: result.docLines.join('\n'), version: result.version @@ -626,11 +633,11 @@ ipcMain.handle('ot:connect', async (_e, projectId: string) => { // Relay collaborator cursor updates to renderer overleafSock.on('serverEvent', (name: string, args: unknown[]) => { if (name === 'clientTracking.clientUpdated') { - mainWindow?.webContents.send('cursor:remoteUpdate', args[0]) + sendToRenderer('cursor:remoteUpdate', args[0]) } else if (name === 'clientTracking.clientDisconnected') { - mainWindow?.webContents.send('cursor:remoteDisconnected', args[0]) + sendToRenderer('cursor:remoteDisconnected', args[0]) } else if (name === 'new-chat-message') { - mainWindow?.webContents.send('chat:newMessage', args[0]) + sendToRenderer('chat:newMessage', args[0]) } }) @@ -705,7 +712,7 @@ ipcMain.handle('ot:joinDoc', async (_e, docId: string) => { if (name === 'otUpdateApplied') { const update = args[0] as { doc?: string; op?: unknown[]; v?: number } | undefined if (update?.doc === docId && update.op) { - mainWindow?.webContents.send('ot:remoteOp', { + sendToRenderer('ot:remoteOp', { docId: update.doc, ops: update.op, version: update.v @@ -1000,7 +1007,7 @@ ipcMain.handle('overleaf:socketCompile', async (_e, mainTexRelPath: string) => { await compilationManager.syncBinaries(fileRefs) return compilationManager.compile(mainTexRelPath, (data) => { - mainWindow?.webContents.send('latex:log', data) + sendToRenderer('latex:log', data) }) }) @@ -1023,6 +1030,7 @@ app.whenReady().then(async () => { }) app.on('window-all-closed', () => { + mainWindow = null ptyInstance?.kill() fileSyncBridge?.stop() fileSyncBridge = null diff --git a/src/renderer/src/App.css b/src/renderer/src/App.css index 760c333..cbf0282 100644 --- a/src/renderer/src/App.css +++ b/src/renderer/src/App.css @@ -102,7 +102,7 @@ html, body, #root { .lattex-x { font-weight: 800; font-style: italic; - color: var(--accent-blue); + color: #4ECDA0; } .welcome-content p { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 88a351b..3259f0d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -291,19 +291,14 @@ export default function App() { <div className="welcome-content"> <div className="welcome-logo"> <svg viewBox="0 0 512 512" width="96" height="96"> - <defs> - <linearGradient id="wcG" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#FFF8E7"/><stop offset="100%" stopColor="#EDE5CE"/></linearGradient> - <linearGradient id="wcC" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#A0782C"/><stop offset="100%" stopColor="#7A5A1E"/></linearGradient> - </defs> - <circle cx="256" cy="256" r="240" fill="#6B5B3E"/> - <path d="M130 190 Q124 390 185 405 L327 405 Q388 390 382 190 Z" fill="url(#wcG)"/> - <ellipse cx="256" cy="190" rx="126" ry="36" fill="url(#wcC)"/> - <path d="M256 175 Q240 165 232 172 Q224 180 236 192 L256 208 L276 192 Q288 180 280 172 Q272 165 256 175Z" fill="#D4B880" opacity="0.6"/> - <ellipse cx="256" cy="190" rx="126" ry="36" fill="none" stroke="#D6CEBC" strokeWidth="3"/> - <path d="M382 230 Q435 235 438 300 Q440 365 390 370" fill="none" stroke="#FFF8E7" strokeWidth="12" strokeLinecap="round"/> - <path d="M216 148 Q210 118 220 95" fill="none" stroke="#FFF8E7" strokeWidth="5" strokeLinecap="round" opacity="0.5"/> - <path d="M256 140 Q250 105 260 80" fill="none" stroke="#FFF8E7" strokeWidth="5" strokeLinecap="round" opacity="0.45"/> - <path d="M296 148 Q290 118 300 95" fill="none" stroke="#FFF8E7" strokeWidth="5" strokeLinecap="round" opacity="0.4"/> + <rect width="512" height="512" rx="80" fill="#6B5B3E"/> + <path d="M148 195 Q142 375 195 395 L317 395 Q370 375 364 195 Z" fill="#FFF8E7" stroke="#EDE5CE" strokeWidth="2"/> + <path d="M364 235 Q410 240 412 305 Q414 365 370 370" fill="none" stroke="#FFF8E7" strokeWidth="14" strokeLinecap="round"/> + <ellipse cx="256" cy="195" rx="108" ry="30" fill="#4ECDA0"/> + <ellipse cx="256" cy="195" rx="108" ry="30" fill="none" stroke="#EDE5CE" strokeWidth="3"/> + <path d="M218 128 L224 108 L230 128 L250 134 L230 140 L224 160 L218 140 L198 134 Z" fill="#4ECDA0" opacity="0.9"/> + <path d="M268 100 L273 84 L278 100 L294 105 L278 110 L273 126 L268 110 L252 105 Z" fill="#4ECDA0" opacity="0.7"/> + <path d="M308 118 L313 102 L318 118 L334 123 L318 128 L313 144 L308 128 L292 123 Z" fill="#4ECDA0" opacity="0.55"/> </svg> </div> <h1>Latte<span className="lattex-x">X</span></h1> |
