Skip to content

Commit 82ec665

Browse files
committed
feat: implement auto-updater notification toast and install functionality
1 parent c143700 commit 82ec665

4 files changed

Lines changed: 124 additions & 1 deletion

File tree

electron/main.cjs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ autoUpdater.autoInstallOnAppQuit = true;
1313
autoUpdater.logger = require('electron-log');
1414
autoUpdater.logger.transports.file.level = 'info';
1515

16+
// Notify renderer when an update has been downloaded and is ready to install
17+
autoUpdater.on('update-downloaded', (info) => {
18+
console.log('[Electron] Update downloaded:', info.version);
19+
if (mainWindow) {
20+
mainWindow.webContents.send('updater:update-ready', {
21+
version: info.version,
22+
releaseName: info.releaseName || ''
23+
});
24+
}
25+
});
26+
1627
const DIST = path.join(__dirname, '../dist');
1728
const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'];
1829

@@ -623,7 +634,7 @@ app.whenReady().then(async () => {
623634
// Check for updates if not in dev mode
624635
if (!isDev) {
625636
try {
626-
autoUpdater.checkForUpdatesAndNotify();
637+
autoUpdater.checkForUpdates();
627638
} catch (error) {
628639
console.error('[Electron] Auto-updater error:', error);
629640
}
@@ -660,3 +671,8 @@ ipcMain.handle('agent:restart', async () => {
660671
return { success: true };
661672
});
662673

674+
// Auto-updater: quit and install the downloaded update
675+
ipcMain.handle('updater:install', () => {
676+
autoUpdater.quitAndInstall();
677+
});
678+

electron/preload.cjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ contextBridge.exposeInMainWorld('electron', {
4949
agent: {
5050
status: () => ipcRenderer.invoke('agent:status'),
5151
restart: () => ipcRenderer.invoke('agent:restart'),
52+
},
53+
54+
// Auto-updater
55+
updater: {
56+
onUpdateReady: (callback) => {
57+
ipcRenderer.on('updater:update-ready', (_event, info) => callback(info));
58+
},
59+
installUpdate: () => ipcRenderer.invoke('updater:install'),
5260
}
5361
});
5462

src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import SpawningNodeDragLayer from './SpawningNodeDragLayer';
44
import BridgeClient from './ai/BridgeClient.jsx';
55
import GlobalContextMenu from './components/GlobalContextMenu.jsx';
66
import UniverseManagerBootstrap from './components/UniverseManagerBootstrap.jsx';
7+
import UpdateToast from './components/UpdateToast.jsx';
78
import useGraphStore from './store/graphStore.jsx';
89
import { DARK_THEME, LIGHT_THEME } from './utils/themeColors.js';
910
import './App.css';
@@ -56,6 +57,7 @@ function App() {
5657
<SpawningNodeDragLayer />
5758
<BridgeClient />
5859
<GlobalContextMenu />
60+
<UpdateToast />
5961
</>
6062
);
6163
}

src/components/UpdateToast.jsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { RotateCcw, X } from 'lucide-react';
3+
import PanelIconButton from './shared/PanelIconButton.jsx';
4+
import { useTheme } from '../hooks/useTheme.js';
5+
6+
/**
7+
* UpdateToast — bottom-right notification shown when an Electron auto-update
8+
* has been downloaded and is ready to install.
9+
*/
10+
export default function UpdateToast() {
11+
const [updateInfo, setUpdateInfo] = useState(null);
12+
const [dismissed, setDismissed] = useState(false);
13+
const [visible, setVisible] = useState(false);
14+
const theme = useTheme();
15+
16+
useEffect(() => {
17+
if (!window.electron?.updater?.onUpdateReady) return;
18+
window.electron.updater.onUpdateReady((info) => {
19+
setUpdateInfo(info);
20+
// Trigger slide-in on next frame
21+
requestAnimationFrame(() => setVisible(true));
22+
});
23+
}, []);
24+
25+
if (!updateInfo || dismissed) return null;
26+
27+
const handleRestart = () => {
28+
window.electron?.updater?.installUpdate();
29+
};
30+
31+
const handleDismiss = () => {
32+
setVisible(false);
33+
// Wait for slide-out animation before unmounting
34+
setTimeout(() => setDismissed(true), 250);
35+
};
36+
37+
const containerStyle = {
38+
position: 'fixed',
39+
bottom: 20,
40+
right: 20,
41+
zIndex: 99999,
42+
display: 'flex',
43+
alignItems: 'center',
44+
gap: 10,
45+
padding: '10px 14px',
46+
borderRadius: 10,
47+
backgroundColor: theme.darkMode ? '#1a1616' : '#f5f0f0',
48+
border: `1px solid ${theme.canvas.border}`,
49+
boxShadow: theme.darkMode
50+
? '0 4px 20px rgba(0,0,0,0.5)'
51+
: '0 4px 20px rgba(0,0,0,0.15)',
52+
transform: visible ? 'translateY(0)' : 'translateY(calc(100% + 30px))',
53+
opacity: visible ? 1 : 0,
54+
transition: 'transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease',
55+
fontFamily: "'EmOne', sans-serif",
56+
pointerEvents: 'auto',
57+
};
58+
59+
const textStyle = {
60+
fontSize: 13,
61+
fontWeight: 600,
62+
color: theme.canvas.textPrimary,
63+
whiteSpace: 'nowrap',
64+
userSelect: 'none',
65+
};
66+
67+
const versionStyle = {
68+
fontSize: 11,
69+
color: theme.canvas.textSecondary,
70+
marginLeft: 2,
71+
};
72+
73+
return (
74+
<div style={containerStyle}>
75+
<span style={textStyle}>
76+
Restart to Update
77+
{updateInfo.version && (
78+
<span style={versionStyle}>v{updateInfo.version}</span>
79+
)}
80+
</span>
81+
<PanelIconButton
82+
icon={RotateCcw}
83+
size={15}
84+
onClick={handleRestart}
85+
title="Restart and install update"
86+
variant="outline"
87+
/>
88+
<PanelIconButton
89+
icon={X}
90+
size={15}
91+
onClick={handleDismiss}
92+
title="Dismiss"
93+
variant="ghost"
94+
/>
95+
</div>
96+
);
97+
}

0 commit comments

Comments
 (0)