Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'feat: add update notifier for Electron desktop app' (#187) from feat/electron-update-notifier into main

scott e9d400ed a05fd49e

+69 -4
+66 -1
electron/main.ts
··· 1 - import { app, BrowserWindow, shell, Menu, ipcMain, net } from 'electron'; 1 + import { app, BrowserWindow, shell, Menu, ipcMain, net, dialog, Notification } from 'electron'; 2 2 import path from 'path'; 3 3 4 4 const TOOLS_URL = process.env.TOOLS_URL || 'https://tools.lobster-hake.ts.net'; ··· 165 165 label: app.name, 166 166 submenu: [ 167 167 { role: 'about' }, 168 + { 169 + label: 'Check for Updates\u2026', 170 + click: () => checkForUpdate(), 171 + }, 168 172 { type: 'separator' }, 169 173 { role: 'hide' }, 170 174 { role: 'hideOthers' }, ··· 225 229 Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 226 230 } 227 231 232 + // --- Update check --- 233 + 234 + interface ReleaseInfo { 235 + available: boolean; 236 + version?: string; 237 + dmg?: { url: string; size: number } | null; 238 + url?: string; 239 + } 240 + 241 + function isNewerVersion(remote: string, current: string): boolean { 242 + const r = remote.replace(/^v/, '').split('.').map(Number); 243 + const c = current.split('.').map(Number); 244 + for (let i = 0; i < 3; i++) { 245 + if ((r[i] || 0) > (c[i] || 0)) return true; 246 + if ((r[i] || 0) < (c[i] || 0)) return false; 247 + } 248 + return false; 249 + } 250 + 251 + async function checkForUpdate(): Promise<void> { 252 + try { 253 + const res = await net.fetch(`${TOOLS_URL}/api/releases/latest`, { 254 + signal: AbortSignal.timeout(5000), 255 + }); 256 + if (!res.ok) return; 257 + const data = (await res.json()) as ReleaseInfo; 258 + if (!data.available || !data.version) return; 259 + 260 + const currentVersion = app.getVersion(); 261 + if (!isNewerVersion(data.version, currentVersion)) return; 262 + 263 + const downloadUrl = data.dmg?.url || data.url; 264 + if (!downloadUrl) return; 265 + 266 + // Show native notification if supported, fall back to dialog 267 + if (Notification.isSupported()) { 268 + const notif = new Notification({ 269 + title: `Tools ${data.version} Available`, 270 + body: `You have ${currentVersion}. Click to download the update.`, 271 + }); 272 + notif.on('click', () => shell.openExternal(downloadUrl)); 273 + notif.show(); 274 + } else if (mainWindow) { 275 + const { response } = await dialog.showMessageBox(mainWindow, { 276 + type: 'info', 277 + title: 'Update Available', 278 + message: `Tools ${data.version} is available (you have ${currentVersion}).`, 279 + buttons: ['Download', 'Later'], 280 + defaultId: 0, 281 + }); 282 + if (response === 0) shell.openExternal(downloadUrl); 283 + } 284 + } catch { 285 + /* update check is best-effort */ 286 + } 287 + } 288 + 228 289 // --- App lifecycle --- 229 290 230 291 app.whenReady().then(async () => { ··· 237 298 } else { 238 299 showError(status); 239 300 } 301 + 302 + // Check for updates 10s after launch, then every 4 hours 303 + setTimeout(checkForUpdate, 10_000); 304 + setInterval(checkForUpdate, 4 * 60 * 60 * 1000); 240 305 241 306 app.on('activate', () => { 242 307 if (BrowserWindow.getAllWindows().length === 0) {
+2 -2
package-lock.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.15.0", 3 + "version": "0.15.1", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "tools", 9 - "version": "0.15.0", 9 + "version": "0.15.1", 10 10 "dependencies": { 11 11 "@tiptap/core": "^2.11.0", 12 12 "@tiptap/extension-collaboration": "^2.11.0",
+1 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.15.0", 3 + "version": "0.15.1", 4 4 "private": true, 5 5 "type": "module", 6 6 "main": "electron/main.js",