Skip to content

Commit 2b6c53d

Browse files
mehdiraizedclaude
andcommitted
feat: auto-updater, menu improvements, onboarding reopen
- Add electron/updater.js: checks GitHub releases API for newer version, shows 'Download Update' dialog, auto-checks after 30s then every 24h - Add 'Check for Updates…' to macOS app menu and Help menu - Add 'Show Intro…' to Help menu (dispatches im-show-onboarding event) - Add Widget menu now has submenu: Widget (⌘⇧W) and Text (⌘⇧T) - onboarding.tsx: listen for im-show-onboarding window event to re-show - Fix: add --publish never to all build:* scripts to prevent electron-builder auto-publishing (conflicts with softprops/action-gh-release) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b047d33 commit 2b6c53d

File tree

5 files changed

+219
-13
lines changed

5 files changed

+219
-13
lines changed

electron/main.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const net = require('net');
2121
const fs = require('fs');
2222

2323
const { buildMenu } = require('./menu');
24+
const { scheduleUpdateCheck } = require('./updater');
2425

2526
// ── Constants ────────────────────────────────────────────────────────────────
2627

@@ -701,6 +702,7 @@ app.whenReady().then(async () => {
701702
await waitForServer(appPort, 90_000, serverAbort.signal);
702703
serverAbort = null;
703704
loadApp();
705+
scheduleUpdateCheck(mainWindow);
704706
} catch (err) {
705707
serverAbort = null;
706708
console.error('[main] startup failed:', err);

electron/menu.js

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const { Menu, shell, app } = require('electron');
44
const { openSettingsWindow } = require('./settings-window');
5+
const { checkForUpdates } = require('./updater');
56

67
/**
78
* Builds and sets the application menu.
@@ -20,6 +21,10 @@ function buildMenu({ isDevMode, mainWindow }) {
2021
label: app.name,
2122
submenu: [
2223
{ role: 'about' },
24+
{
25+
label: 'Check for Updates…',
26+
click() { checkForUpdates(mainWindow, true); },
27+
},
2328
{ type: 'separator' },
2429
{
2530
label: 'Settings…',
@@ -57,15 +62,30 @@ function buildMenu({ isDevMode, mainWindow }) {
5762
{ type: 'separator' },
5863
{
5964
label: 'Add Widget',
60-
accelerator: 'CmdOrCtrl+Shift+W',
61-
click() {
62-
if (mainWindow) {
63-
// Click the Add button in the web app header to open the dropdown
64-
mainWindow.webContents.executeJavaScript(
65-
`document.querySelector('[data-add-menu-trigger]')?.click()`
66-
);
67-
}
68-
},
65+
submenu: [
66+
{
67+
label: 'Widget',
68+
accelerator: 'CmdOrCtrl+Shift+W',
69+
click() {
70+
if (mainWindow) {
71+
mainWindow.webContents.executeJavaScript(
72+
`document.querySelector('[data-add-menu-trigger]')?.click()`
73+
);
74+
}
75+
},
76+
},
77+
{
78+
label: 'Text',
79+
accelerator: 'CmdOrCtrl+Shift+T',
80+
click() {
81+
if (mainWindow) {
82+
mainWindow.webContents.executeJavaScript(
83+
`document.querySelector('[data-add-text-trigger]')?.click()`
84+
);
85+
}
86+
},
87+
},
88+
],
6989
},
7090
{ type: 'separator' },
7191
{
@@ -147,6 +167,21 @@ function buildMenu({ isDevMode, mainWindow }) {
147167
{
148168
role: 'help',
149169
submenu: [
170+
{
171+
label: 'Show Intro…',
172+
click() {
173+
if (mainWindow) {
174+
mainWindow.webContents.executeJavaScript(
175+
`window.dispatchEvent(new CustomEvent('im-show-onboarding'))`
176+
);
177+
}
178+
},
179+
},
180+
{
181+
label: 'Check for Updates…',
182+
click() { checkForUpdates(mainWindow, true); },
183+
},
184+
{ type: 'separator' },
150185
{
151186
label: 'Infinite Monitor on GitHub',
152187
click() {

electron/updater.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
'use strict';
2+
3+
const https = require('https');
4+
const { app, dialog, shell } = require('electron');
5+
6+
const RELEASES_API_URL = 'https://api.github.com/repos/mehdiraized/infinite-monitor-desktop/releases/latest';
7+
const USER_AGENT = 'infinite-monitor-desktop/1.0.0';
8+
9+
// 24 hours in milliseconds
10+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
11+
// Initial delay before first background check (30 seconds)
12+
const INITIAL_DELAY_MS = 30 * 1000;
13+
14+
/**
15+
* Parses a semver string like "v1.2.3" or "1.2.3" into [major, minor, patch].
16+
* Returns [0, 0, 0] if parsing fails.
17+
* @param {string} version
18+
* @returns {[number, number, number]}
19+
*/
20+
function parseSemver(version) {
21+
const clean = version.replace(/^v/, '').trim();
22+
const parts = clean.split('.').map((p) => parseInt(p, 10) || 0);
23+
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
24+
}
25+
26+
/**
27+
* Returns true if `remote` is strictly newer than `local`.
28+
* Compares major, then minor, then patch numerically.
29+
* @param {string} local e.g. "1.0.0"
30+
* @param {string} remote e.g. "v1.2.0"
31+
* @returns {boolean}
32+
*/
33+
function isNewerVersion(local, remote) {
34+
const [lMaj, lMin, lPat] = parseSemver(local);
35+
const [rMaj, rMin, rPat] = parseSemver(remote);
36+
37+
if (rMaj !== lMaj) return rMaj > lMaj;
38+
if (rMin !== lMin) return rMin > lMin;
39+
return rPat > lPat;
40+
}
41+
42+
/**
43+
* Fetches the latest release info from GitHub.
44+
* Resolves with { tag_name, html_url } or rejects on network/parse error.
45+
* @returns {Promise<{ tag_name: string, html_url: string }>}
46+
*/
47+
function fetchLatestRelease() {
48+
return new Promise((resolve, reject) => {
49+
const req = https.get(
50+
RELEASES_API_URL,
51+
{
52+
headers: {
53+
'User-Agent': USER_AGENT,
54+
Accept: 'application/vnd.github+json',
55+
},
56+
},
57+
(res) => {
58+
let body = '';
59+
res.on('data', (chunk) => { body += chunk; });
60+
res.on('end', () => {
61+
try {
62+
const json = JSON.parse(body);
63+
if (!json.tag_name) {
64+
reject(new Error('No tag_name in response'));
65+
return;
66+
}
67+
resolve({ tag_name: json.tag_name, html_url: json.html_url });
68+
} catch (err) {
69+
reject(new Error(`Failed to parse GitHub response: ${err.message}`));
70+
}
71+
});
72+
}
73+
);
74+
75+
req.on('error', (err) => {
76+
reject(new Error(`Network error: ${err.message}`));
77+
});
78+
79+
// 10 second timeout
80+
req.setTimeout(10_000, () => {
81+
req.destroy(new Error('Request timed out'));
82+
});
83+
});
84+
}
85+
86+
/**
87+
* Checks GitHub for a newer release and prompts the user if one is found.
88+
*
89+
* @param {Electron.BrowserWindow | null} mainWindow
90+
* @param {boolean} isManual When true, always show a result dialog (even if up-to-date or on error).
91+
*/
92+
async function checkForUpdates(mainWindow, isManual = false) {
93+
const currentVersion = app.getVersion();
94+
95+
try {
96+
const { tag_name, html_url } = await fetchLatestRelease();
97+
const latestVersion = tag_name.replace(/^v/, '');
98+
99+
if (isNewerVersion(currentVersion, tag_name)) {
100+
// Update is available — prompt the user
101+
const { response } = await dialog.showMessageBox({
102+
type: 'info',
103+
title: 'Update Available',
104+
message: `Infinite Monitor v${latestVersion} is available.\nYou're running v${currentVersion}.`,
105+
buttons: ['Download Update', 'Later'],
106+
defaultId: 0,
107+
cancelId: 1,
108+
});
109+
110+
if (response === 0) {
111+
shell.openExternal(html_url);
112+
}
113+
} else if (isManual) {
114+
// Already on the latest version — inform the user only when they asked manually
115+
await dialog.showMessageBox({
116+
type: 'info',
117+
title: 'No Update Available',
118+
message: `You're up to date! v${currentVersion} is the latest version.`,
119+
buttons: ['OK'],
120+
});
121+
}
122+
} catch (err) {
123+
console.error('[updater] check failed:', err.message);
124+
if (isManual) {
125+
await dialog.showMessageBox({
126+
type: 'warning',
127+
title: 'Update Check Failed',
128+
message: 'Could not check for updates. Check your internet connection.',
129+
buttons: ['OK'],
130+
});
131+
}
132+
}
133+
}
134+
135+
/**
136+
* Schedules automatic (silent) update checks.
137+
* First check runs after INITIAL_DELAY_MS (30 s), then repeats every 24 h.
138+
*
139+
* @param {Electron.BrowserWindow | null} mainWindow
140+
*/
141+
function scheduleUpdateCheck(mainWindow) {
142+
// Initial check after 30 seconds
143+
setTimeout(() => {
144+
checkForUpdates(mainWindow, false);
145+
146+
// Subsequent checks every 24 hours
147+
setInterval(() => {
148+
checkForUpdates(mainWindow, false);
149+
}, CHECK_INTERVAL_MS);
150+
}, INITIAL_DELAY_MS);
151+
}
152+
153+
module.exports = { checkForUpdates, scheduleUpdateCheck };

overlay/src/components/onboarding.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,22 @@ export function Onboarding() {
301301
}
302302
}, []);
303303

304+
// Allow the Electron menu ("Show Intro…") to re-trigger the onboarding
305+
// without a full page reload, by dispatching a custom window event.
306+
useEffect(() => {
307+
if (typeof window === "undefined") return;
308+
function handleShowOnboarding() {
309+
setSlide(0);
310+
setLeaving(false);
311+
setDir(1);
312+
setVisible(true);
313+
}
314+
window.addEventListener("im-show-onboarding", handleShowOnboarding);
315+
return () => {
316+
window.removeEventListener("im-show-onboarding", handleShowOnboarding);
317+
};
318+
}, []);
319+
304320
const dismiss = useCallback(() => {
305321
localStorage.setItem(ONBOARDING_KEY, "1");
306322
setVisible(false);

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
"release": "node scripts/release.js",
1717
"predev": "node scripts/apply-overlay.js",
1818
"dev": "cross-env NODE_ENV=development electron .",
19-
"build": "node scripts/apply-overlay.js && pnpm run generate-icons && pnpm run prepare-web && electron-builder",
20-
"build:mac": "node scripts/apply-overlay.js && pnpm run generate-icons && pnpm run prepare-web && electron-builder --mac",
21-
"build:win": "node scripts/apply-overlay.js && pnpm run generate-icons && pnpm run prepare-web && electron-builder --win",
22-
"build:linux": "node scripts/apply-overlay.js && pnpm run generate-icons && pnpm run prepare-web && electron-builder --linux",
19+
"build": "node scripts/apply-overlay.js && pnpm run generate-icons && pnpm run prepare-web && electron-builder --publish never",
20+
"build:mac": "node scripts/apply-overlay.js && pnpm run generate-icons && pnpm run prepare-web && electron-builder --mac --publish never",
21+
"build:win": "node scripts/apply-overlay.js && pnpm run generate-icons && pnpm run prepare-web && electron-builder --win --publish never",
22+
"build:linux": "node scripts/apply-overlay.js && pnpm run generate-icons && pnpm run prepare-web && electron-builder --linux --publish never",
2323
"generate-icons": "node scripts/generate-icons.js",
2424
"prepare-web": "node scripts/prepare-web.js",
2525
"apply-overlay": "node scripts/apply-overlay.js",

0 commit comments

Comments
 (0)