From bd071702bcf42be2febf06c287f020b5852c1af0 Mon Sep 17 00:00:00 2001 From: Tony Yu Date: Wed, 14 Oct 2026 11:27:12 +0800 Subject: [PATCH 1/2] feat: check for updates from GitHub releases (download + open DMG) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → 'Check for updates' queries the latest GitHub release, compares to the running version, and if newer downloads the DMG to Downloads and opens (mounts) it to install. No signing/auto-install (ad-hoc build) — user drags to Applications. --- app/electron/main.ts | 56 ++++++++++++++++++++++++++++++++ app/electron/preload.ts | 6 ++++ app/renderer/main.tsx | 4 +++ app/renderer/settings-dialog.tsx | 47 +++++++++++++++++++++++++++ 4 files changed, 113 insertions(+) diff --git a/app/electron/main.ts b/app/electron/main.ts index 89877ae..55f846d 100644 --- a/app/electron/main.ts +++ b/app/electron/main.ts @@ -142,10 +142,66 @@ const createWindow = (token: string): void => { } } +// GitHub repo that hosts releases (public; no token needed for the releases API). +const REPO = 'FishPain/fish-notes' + +// Compare dotted numeric versions: true if a > b (e.g. "0.1.2" > "0.1.1"). +const isNewer = (a: string, b: string): boolean => { + const pa = a.split('.').map(Number) + const pb = b.split('.').map(Number) + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0) + } + return false +} + +// Query the latest GitHub release and compare to the running version. +const checkForUpdate = async (): Promise<{ + current: string + latest: string | null + hasUpdate: boolean + dmgUrl?: string + releaseUrl?: string +}> => { + const current = app.getVersion() + try { + const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, { + headers: { 'User-Agent': 'FishNotes', Accept: 'application/vnd.github+json' } + }) + if (!res.ok) return { current, latest: null, hasUpdate: false } + const rel = (await res.json()) as { + tag_name: string + html_url: string + assets: { name: string; browser_download_url: string }[] + } + const latest = rel.tag_name.replace(/^v/, '') + const dmg = rel.assets.find((a) => a.name.endsWith('.dmg')) + return { current, latest, hasUpdate: isNewer(latest, current), dmgUrl: dmg?.browser_download_url, releaseUrl: rel.html_url } + } catch { + return { current, latest: null, hasUpdate: false } + } +} + +// Download a release DMG to the Downloads folder and open (mount) it for the user to install. +const downloadUpdate = async (url: string): Promise<{ ok: boolean; path?: string }> => { + try { + const res = await fetch(url, { headers: { 'User-Agent': 'FishNotes' } }) + if (!res.ok) return { ok: false } + const dest = join(app.getPath('downloads'), url.split('/').pop() || 'Fish Notes.dmg') + await writeFile(dest, Buffer.from(await res.arrayBuffer())) + shell.openPath(dest) + return { ok: true, path: dest } + } catch { + return { ok: false } + } +} + app.whenReady().then(() => { const token = loadOrCreateToken(app.getPath('userData')) ipcMain.handle('capture-region', captureRegion) ipcMain.handle('save-image', (_e, { dataUrl, name }: { dataUrl: string; name: string }) => saveImage(dataUrl, name)) + ipcMain.handle('updates:check', () => checkForUpdate()) + ipcMain.handle('updates:download', (_e, url: string) => downloadUpdate(url)) // Settings: read effective config (defaults + env + saved), write overrides to // userData/settings.json, test the proxy, and relaunch to apply. diff --git a/app/electron/preload.ts b/app/electron/preload.ts index bf3d37d..1a9f1da 100644 --- a/app/electron/preload.ts +++ b/app/electron/preload.ts @@ -14,6 +14,12 @@ contextBridge.exposeInMainWorld('engine', { // App version (from package.json via app.getVersion() in main), shown in the sidebar. contextBridge.exposeInMainWorld('appInfo', { version: arg('app-version') }) +// Check GitHub releases for a newer version and download+open its DMG. +contextBridge.exposeInMainWorld('updates', { + check: () => ipcRenderer.invoke('updates:check'), + download: (url: string) => ipcRenderer.invoke('updates:download', url) +}) + // Native screen-region capture (main runs macOS `screencapture`). contextBridge.exposeInMainWorld('capture', { region: () => ipcRenderer.invoke('capture-region'), diff --git a/app/renderer/main.tsx b/app/renderer/main.tsx index 4067fe4..30beb5b 100644 --- a/app/renderer/main.tsx +++ b/app/renderer/main.tsx @@ -36,6 +36,10 @@ declare global { interface Window { engine: { baseUrl: string; token: string } appInfo: { version: string } + updates: { + check: () => Promise<{ current: string; latest: string | null; hasUpdate: boolean; dmgUrl?: string; releaseUrl?: string }> + download: (url: string) => Promise<{ ok: boolean; path?: string }> + } capture: { region: () => Promise<{ cancelled?: boolean; pngBase64?: string; error?: string; appName?: string; url?: string; windowTitle?: string }> saveImage: (dataUrl: string, name: string) => Promise<{ saved?: boolean; path?: string; cancelled?: boolean }> diff --git a/app/renderer/settings-dialog.tsx b/app/renderer/settings-dialog.tsx index cfc0472..8f14b89 100644 --- a/app/renderer/settings-dialog.tsx +++ b/app/renderer/settings-dialog.tsx @@ -12,12 +12,17 @@ export const SettingsDialog = ({ open, onClose }: { open: boolean; onClose: () = const [test, setTest] = useState<{ ok: boolean; count?: number; status?: number } | null>(null) const [testing, setTesting] = useState(false) const [saved, setSaved] = useState(false) + const [update, setUpdate] = useState> | null>(null) + const [checking, setChecking] = useState(false) + const [dl, setDl] = useState<'idle' | 'downloading' | 'opened' | 'error'>('idle') // Load config when opened, then quietly fetch the model list to fill the dropdowns. useEffect(() => { if (!open) return setTest(null) setSaved(false) + setUpdate(null) + setDl('idle') window.settings.get().then(async (c) => { setCfg(c) const r = await window.settings.test(c.baseUrl, c.apiKey) @@ -40,6 +45,20 @@ export const SettingsDialog = ({ open, onClose }: { open: boolean; onClose: () = setSaved(true) } + const checkUpdates = async (): Promise => { + setChecking(true) + setDl('idle') + setUpdate(await window.updates.check()) + setChecking(false) + } + + const downloadUpdate = async (): Promise => { + if (!update?.dmgUrl) return + setDl('downloading') + const r = await window.updates.download(update.dmgUrl) + setDl(r.ok ? 'opened' : 'error') + } + return ( Settings @@ -89,6 +108,34 @@ export const SettingsDialog = ({ open, onClose }: { open: boolean; onClose: () = Open data folder + + + + {update && !update.hasUpdate && ( + + {update.latest ? `You're on the latest version (v${update.current}).` : "Couldn't check right now — try again later."} + + )} + {update?.hasUpdate && update.dmgUrl && ( + + {dl === 'downloading' ? 'Downloading…' : 'Download'} + + } + > + Update available: v{update.latest} (you have v{update.current}). + + )} + {dl === 'opened' && ( + Installer opened — drag Fish Notes to Applications, then reopen. First launch: right-click → Open. + )} + {dl === 'error' && Download failed — try again later.} + + {test && ( {test.ok ? `Connected — ${test.count} models available` : `Failed${test.status ? ` (HTTP ${test.status})` : ' to reach the proxy'}`} From 7721a87521e207ee68dc9207fc9748627a5f132b Mon Sep 17 00:00:00 2001 From: Tony Yu Date: Wed, 14 Oct 2026 13:39:48 +0800 Subject: [PATCH 2/2] rename to Fish Notes --- app/renderer/index.html | 2 +- docs/design/plans/2026-09-18-phase1b-capturer.md | 2 +- docs/design/plans/2026-09-18-phase2a-electron-shell.md | 6 +++--- .../specs/2026-09-18-web-and-screen-note-capture-design.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/renderer/index.html b/app/renderer/index.html index 4045a84..5c1a530 100644 --- a/app/renderer/index.html +++ b/app/renderer/index.html @@ -2,7 +2,7 @@ - Canvas Notes + Fish Notes
diff --git a/docs/design/plans/2026-09-18-phase1b-capturer.md b/docs/design/plans/2026-09-18-phase1b-capturer.md index 765fbba..62913f9 100644 --- a/docs/design/plans/2026-09-18-phase1b-capturer.md +++ b/docs/design/plans/2026-09-18-phase1b-capturer.md @@ -548,7 +548,7 @@ The pure modules above are tested; this task is the browser wiring, verified by ```json { "manifest_version": 3, - "name": "Canvas Notes Capturer", + "name": "Fish Notes Capturer", "version": "0.1.0", "permissions": ["activeTab", "scripting", "storage", "contextMenus"], "host_permissions": ["http://localhost:7645/*"], diff --git a/docs/design/plans/2026-09-18-phase2a-electron-shell.md b/docs/design/plans/2026-09-18-phase2a-electron-shell.md index cda588c..4c8bfb1 100644 --- a/docs/design/plans/2026-09-18-phase2a-electron-shell.md +++ b/docs/design/plans/2026-09-18-phase2a-electron-shell.md @@ -311,7 +311,7 @@ No unit tests (UI wiring, verified by running the app). Keep components small; M - Canvas Notes + Fish Notes
@@ -416,7 +416,7 @@ export const App = (): React.ReactElement => { return ( - Canvas Notes + Fish Notes