Skip to content

Commit e3c07a3

Browse files
authored
Merge pull request #74 from grimmerk/feat/custom-update-ui
feat: custom update UI in Settings popup
2 parents 0e73bee + 7beb9f7 commit e3c07a3

5 files changed

Lines changed: 106 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.0.49
4+
5+
- Custom update UI: "Check for Update" + "Install & Restart" in Settings popup
6+
- No auto-popup dialogs — update check is manual only
7+
38
## 1.0.48
49

510
- In-app auto-update for non-MAS builds (via update-electron-app)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "CodeV",
33
"productName": "CodeV",
4-
"version": "1.0.48",
4+
"version": "1.0.49",
55
"description": "Quick switcher for VS Code, Cursor, and Claude Code sessions",
66
"repository": {
77
"type": "git",

src/main.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
app,
3+
autoUpdater,
34
BrowserWindow,
45
dialog,
56
globalShortcut,
@@ -970,7 +971,27 @@ const trayToggleEvtHandler = async () => {
970971
repo: 'grimmerk/codev',
971972
},
972973
updateInterval: '1 hour',
973-
notifyUser: true,
974+
notifyUser: false,
975+
});
976+
977+
// Forward autoUpdater events to renderer for custom UI
978+
autoUpdater.on('checking-for-update', () => {
979+
switcherWindow?.webContents.send('update-status', { status: 'checking' });
980+
});
981+
autoUpdater.on('update-available', () => {
982+
switcherWindow?.webContents.send('update-status', { status: 'downloading' });
983+
});
984+
autoUpdater.on('update-not-available', () => {
985+
switcherWindow?.webContents.send('update-status', { status: 'up-to-date' });
986+
});
987+
autoUpdater.on('update-downloaded', (_event: any, releaseNotes: string, releaseName: string) => {
988+
switcherWindow?.webContents.send('update-status', { status: 'ready', releaseName });
989+
});
990+
autoUpdater.on('error', (err: Error) => {
991+
if (isDebug) {
992+
console.error('Auto-update error:', err);
993+
}
994+
switcherWindow?.webContents.send('update-status', { status: 'error', error: err.message });
974995
});
975996
} catch (e) {
976997
if (isDebug) {
@@ -1557,6 +1578,27 @@ ipcMain.handle('get-app-version', () => {
15571578
return app.getVersion();
15581579
});
15591580

1581+
// Auto-update IPC handlers
1582+
ipcMain.on('check-for-update', () => {
1583+
if (!isMAS()) {
1584+
try {
1585+
autoUpdater.checkForUpdates();
1586+
} catch (e) {
1587+
if (isDebug) {
1588+
console.error('Manual update check failed:', e);
1589+
}
1590+
switcherWindow?.webContents.send('update-status', {
1591+
status: 'error',
1592+
error: e instanceof Error ? e.message : 'Update check failed',
1593+
});
1594+
}
1595+
}
1596+
});
1597+
1598+
ipcMain.on('install-update', () => {
1599+
autoUpdater.quitAndInstall();
1600+
});
1601+
15601602
// Login item settings are now controlled by the user via Settings UI toggle
15611603
ipcMain.handle('get-login-item-settings', () => {
15621604
return app.getLoginItemSettings();

src/popup.tsx

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ const PopupDefaultExample = ({
7171
});
7272
const [editingShortcut, setEditingShortcut] = useState<string | null>(null);
7373
const [shortcutError, setShortcutError] = useState('');
74+
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'downloading' | 'ready' | 'up-to-date' | 'error'>('idle');
75+
const [updateReleaseName, setUpdateReleaseName] = useState('');
76+
const [updateTimer, setUpdateTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
7477

7578
useEffect(() => {
7679
(window as any).electronAPI.getAppVersion().then((version: string) => {
@@ -107,6 +110,12 @@ const PopupDefaultExample = ({
107110
(window as any).electronAPI.getShortcuts().then((s: typeof shortcuts) => {
108111
if (s) setShortcuts(s);
109112
});
113+
(window as any).electronAPI.onUpdateStatus((_event: any, data: any) => {
114+
setUpdateStatus(data.status);
115+
if (data.releaseName) setUpdateReleaseName(data.releaseName);
116+
// Clear timeout when we get a real response
117+
setUpdateTimer((prev) => { if (prev) clearTimeout(prev); return null; });
118+
});
110119
}, []);
111120

112121
useEffect(() => {
@@ -205,6 +214,14 @@ const PopupDefaultExample = ({
205214
setShortcutError('');
206215
};
207216

217+
const triggerUpdateCheck = () => {
218+
setUpdateStatus('checking');
219+
if (updateTimer) clearTimeout(updateTimer);
220+
const timer = setTimeout(() => setUpdateStatus('error'), 30000);
221+
setUpdateTimer(timer);
222+
(window as any).electronAPI.checkForUpdate();
223+
};
224+
208225
const shortcutRows = [
209226
{ key: 'quickSwitcher', label: 'Quick Switcher' },
210227
{ key: 'aiInsight', label: 'AI Insight' },
@@ -229,9 +246,44 @@ const PopupDefaultExample = ({
229246
}}
230247
>
231248
<style>{`@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }`}</style>
232-
{/* Version + Quit — compact top bar */}
249+
{/* Version + Update + Quit — compact top bar */}
233250
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '6px 16px', borderBottom: '1px solid #333' }}>
234-
<span style={{ fontSize: '11px', color: '#666' }}>v{appVersion}</span>
251+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
252+
<span style={{ fontSize: '11px', color: '#666' }}>v{appVersion}</span>
253+
{updateStatus === 'idle' && (
254+
<span
255+
onClick={triggerUpdateCheck}
256+
style={{ fontSize: '10px', color: THEME.primary, cursor: 'pointer' }}
257+
>
258+
Check for Update
259+
</span>
260+
)}
261+
{updateStatus === 'checking' && (
262+
<span style={{ fontSize: '10px', color: '#888' }}>Checking...</span>
263+
)}
264+
{updateStatus === 'downloading' && (
265+
<span style={{ fontSize: '10px', color: '#888' }}>Downloading...</span>
266+
)}
267+
{updateStatus === 'ready' && (
268+
<span
269+
onClick={() => (window as any).electronAPI.installUpdate()}
270+
style={{ fontSize: '10px', color: '#4CAF50', cursor: 'pointer', fontWeight: 600 }}
271+
>
272+
{updateReleaseName ? `${updateReleaseName} ready — ` : ''}Install & Restart
273+
</span>
274+
)}
275+
{updateStatus === 'up-to-date' && (
276+
<span style={{ fontSize: '10px', color: '#888' }}>Latest</span>
277+
)}
278+
{updateStatus === 'error' && (
279+
<span
280+
onClick={triggerUpdateCheck}
281+
style={{ fontSize: '10px', color: '#e05252', cursor: 'pointer' }}
282+
>
283+
Retry
284+
</span>
285+
)}
286+
</div>
235287
<span
236288
onClick={() => closeAppClick()}
237289
style={{ fontSize: '11px', color: '#CC6666', cursor: 'pointer', textDecoration: 'underline' }}

src/preload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
2626
setLeftClickBehavior: (behavior: string) => ipcRenderer.send('set-left-click-behavior', behavior),
2727

2828
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
29+
checkForUpdate: () => ipcRenderer.send('check-for-update'),
30+
installUpdate: () => ipcRenderer.send('install-update'),
31+
onUpdateStatus: (callback: any) => ipcRenderer.on('update-status', callback),
2932
getSessionTerminalApp: () => ipcRenderer.invoke('get-session-terminal-app'),
3033
setSessionTerminalApp: (app: string) => ipcRenderer.send('set-session-terminal-app', app),
3134
getSessionTerminalMode: () => ipcRenderer.invoke('get-session-terminal-mode'),

0 commit comments

Comments
 (0)