summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhaoyuren <13851610112@163.com>2026-03-13 01:37:50 -0500
committerhaoyuren <13851610112@163.com>2026-03-13 01:37:50 -0500
commit1f68cb8a5de45c8b697ca74085cd5aae2c361787 (patch)
tree3cdb12870eadcd601c88af21efa27ec596b476eb
parent8b3b3be550307598e84c59e3d708e6ee9a3e1beb (diff)
Update logo, app icon, fix shutdown crash, add README
- Simplified logo: cosmic latte cup on brown background with turquoise liquid and sparkle stars - Square app icon (.icns) for macOS dock - Fix crash on window close: guard all IPC sends against destroyed window - Include ws/chokidar/diff-match-patch in packaged app - Add README with features and install instructions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-rw-r--r--README.md50
-rw-r--r--electron-builder.yml20
-rw-r--r--resources/icon.icnsbin0 -> 168312 bytes
-rw-r--r--resources/logo.svg35
-rw-r--r--src/main/index.ts28
-rw-r--r--src/renderer/src/App.css2
-rw-r--r--src/renderer/src/App.tsx21
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
new file mode 100644
index 0000000..cf000bc
--- /dev/null
+++ b/resources/icon.icns
Binary files differ
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>