diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0754c0d1c..553d52797 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "effect": "catalog:", - "electron": "40.6.0", + "electron": "40.7.0", "electron-updater": "^6.6.2" }, "devDependencies": { diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index c3dba6016..2556fc3fc 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -14,6 +14,7 @@ import { nativeTheme, protocol, shell, + MenuItem, } from "electron"; import type { MenuItemConstructorOptions } from "electron"; import * as Effect from "effect/Effect"; @@ -277,6 +278,8 @@ let updateCheckInFlight = false; let updateDownloadInFlight = false; let updaterConfigured = false; let updateState: DesktopUpdateState = initialUpdateState(); +const updateStateListeners = new Set<(state: DesktopUpdateState) => void>(); +updateStateListeners.add(() => emitUpdateState()); function resolveUpdaterErrorContext(): DesktopUpdateErrorContext { if (updateDownloadInFlight) return "download"; @@ -559,18 +562,77 @@ async function checkForUpdatesFromMenu(): Promise { } } +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DEFAULT = "Check for Updates..."; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_CHECKING = "Checking for Updates..."; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_IDLE = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DEFAULT; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DISABLED = "Updates unavailable"; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADING = "Downloading update..."; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADED = "Update downloaded"; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_AVAILABLE = "Update available"; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_UP_TO_DATE = "You're up to date!"; +const CHECK_FOR_UPDATES_MENU_ITEM_LABEL_ERROR = "Update check failed"; +function makeCheckForUpdatesMenuItem(): MenuItem { + return new MenuItem({ + label: CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DEFAULT, + click: async () => await handleCheckForUpdatesMenuClick(), + }); +} +const checkForUpdatesMenuItemInAppMenu = makeCheckForUpdatesMenuItem(); +const checkForUpdatesMenuItemInHelpMenu = makeCheckForUpdatesMenuItem(); + +function updateCheckForUpdatesMenuItem(menuItem: MenuItem, state: DesktopUpdateState): void { + switch (state.status) { + case "checking": + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_CHECKING; + menuItem.enabled = false; + break; + case "available": + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_AVAILABLE; + menuItem.enabled = false; + break; + case "downloading": + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADING; + menuItem.enabled = false; + break; + case "downloaded": + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DOWNLOADED; + menuItem.enabled = false; + break; + case "disabled": + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_DISABLED; + menuItem.enabled = false; + break; + case "error": + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_ERROR; + menuItem.enabled = false; + break; + case "up-to-date": + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_UP_TO_DATE; + menuItem.enabled = false; + break; + case "idle": + menuItem.label = CHECK_FOR_UPDATES_MENU_ITEM_LABEL_IDLE; + menuItem.enabled = true; + break; + } +} + +updateStateListeners.add((state) => { + updateCheckForUpdatesMenuItem(checkForUpdatesMenuItemInAppMenu, state); + updateCheckForUpdatesMenuItem(checkForUpdatesMenuItemInHelpMenu, state); +}); + +let applicationMenu: Menu | null = null; + function configureApplicationMenu(): void { - const template: MenuItemConstructorOptions[] = []; + const template: (MenuItemConstructorOptions | MenuItem)[] = []; if (process.platform === "darwin") { template.push({ label: app.name, - submenu: [ + submenu: Menu.buildFromTemplate([ { role: "about" }, - { - label: "Check for Updates...", - click: () => handleCheckForUpdatesMenuClick(), - }, + checkForUpdatesMenuItemInAppMenu, { type: "separator" }, { label: "Settings...", @@ -585,7 +647,7 @@ function configureApplicationMenu(): void { { role: "unhide" }, { type: "separator" }, { role: "quit" }, - ], + ]), }); } @@ -625,16 +687,12 @@ function configureApplicationMenu(): void { { role: "windowMenu" }, { role: "help", - submenu: [ - { - label: "Check for Updates...", - click: () => handleCheckForUpdatesMenuClick(), - }, - ], + submenu: Menu.buildFromTemplate([checkForUpdatesMenuItemInHelpMenu]), }, ); - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + applicationMenu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(applicationMenu); } function resolveResourcePath(fileName: string): string | null { @@ -727,7 +785,9 @@ function emitUpdateState(): void { function setUpdateState(patch: Partial): void { updateState = { ...updateState, ...patch }; - emitUpdateState(); + for (const listener of updateStateListeners) { + listener(updateState); + } } function shouldEnableAutoUpdates(): boolean { diff --git a/bun.lock b/bun.lock index b8e36149f..334eaeb79 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ "version": "0.0.10", "dependencies": { "effect": "catalog:", - "electron": "40.6.0", + "electron": "40.7.0", "electron-updater": "^6.6.2", }, "devDependencies": { @@ -1012,7 +1012,7 @@ "effect": ["effect@https://pkg.pr.new/Effect-TS/effect-smol/effect@8881a9b", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }], - "electron": ["electron@40.6.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-ett8W+yOFGDuM0vhJMamYSkrbV3LoaffzJd9GfjI96zRAxyrNqUSKqBpf/WGbQCweDxX2pkUCUfrv4wwKpsFZA=="], + "electron": ["electron@40.7.0", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-oQe76S/3V1rcb0+i45hAxnCH8udkRZSaHUNwglzNAEKbB94LSJ1qwbFo8+uRc2UsYZgCqSIMRcyX40GyOkD+Xw=="], "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="],