diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index f88743dc4bbf05e9c939fd7cb5e24eb052afdd7c..54a521f9f0253ac2d829b4552047b89fb46640f6 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -15,6 +15,7 @@ import { NetService } from "@t3tools/shared/Net"; import { RotatingFileSink } from "@t3tools/shared/logging"; import { showDesktopConfirmDialog } from "./confirmDialog"; import { fixPath } from "./fixPath"; +import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; fixPath(); @@ -425,11 +426,28 @@ function dispatchMenuAction(action: string): void { } function handleCheckForUpdatesMenuClick(): void { - if (!shouldEnableAutoUpdates()) { + const disabledReason = getAutoUpdateDisabledReason({ + isDevelopment, + isPackaged: app.isPackaged, + platform: process.platform, + appImage: process.env.APPIMAGE, + disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1", + }); + if (disabledReason) { console.info("[desktop-updater] Manual update check requested, but updates are disabled."); + void dialog.showMessageBox({ + type: "info", + title: "Updates unavailable", + message: "Automatic updates are not available right now.", + detail: disabledReason, + buttons: ["OK"], + }); return; } + if (!BrowserWindow.getAllWindows().length) { + mainWindow = createWindow(); + } void checkForUpdates("menu"); } @@ -562,17 +580,15 @@ function setUpdateState(patch: Partial): void { } function shouldEnableAutoUpdates(): boolean { - if (isDevelopment || !app.isPackaged) { - return false; - } - if (process.env.T3CODE_DISABLE_AUTO_UPDATE === "1") { - return false; - } - if (process.platform === "linux" && !process.env.APPIMAGE) { - // electron-updater only supports Linux auto-updates when running from AppImage. - return false; - } - return true; + return ( + getAutoUpdateDisabledReason({ + isDevelopment, + isPackaged: app.isPackaged, + platform: process.platform, + appImage: process.env.APPIMAGE, + disabledByEnv: process.env.T3CODE_DISABLE_AUTO_UPDATE === "1", + }) === null + ); } async function checkForUpdates(reason: string): Promise { @@ -719,11 +735,16 @@ function configureAutoUpdater(): void { }); autoUpdater.on("download-progress", (progress) => { const percent = Math.floor(progress.percent); - setUpdateState({ - status: "downloading", - downloadPercent: progress.percent, - message: null, - }); + if ( + shouldBroadcastDownloadProgress(updateState, progress.percent) || + updateState.message !== null + ) { + setUpdateState({ + status: "downloading", + downloadPercent: progress.percent, + message: null, + }); + } const milestone = percent - (percent % 10); if (milestone > lastLoggedDownloadMilestone) { lastLoggedDownloadMilestone = milestone; diff --git a/apps/desktop/src/updateState.test.ts b/apps/desktop/src/updateState.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b3fa7cb82ed21b5bacc8f0579601ab2cf329b91 --- /dev/null +++ b/apps/desktop/src/updateState.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import type { DesktopUpdateState } from "@t3tools/contracts"; + +import { getAutoUpdateDisabledReason, shouldBroadcastDownloadProgress } from "./updateState"; + +const baseState: DesktopUpdateState = { + enabled: true, + status: "idle", + currentVersion: "1.0.0", + availableVersion: null, + downloadedVersion: null, + downloadPercent: null, + checkedAt: null, + message: null, +}; + +describe("shouldBroadcastDownloadProgress", () => { + it("broadcasts the first downloading progress update", () => { + expect( + shouldBroadcastDownloadProgress( + { ...baseState, status: "downloading", downloadPercent: null }, + 1, + ), + ).toBe(true); + }); + + it("skips progress updates within the same 10% bucket", () => { + expect( + shouldBroadcastDownloadProgress( + { ...baseState, status: "downloading", downloadPercent: 11.2 }, + 18.7, + ), + ).toBe(false); + }); + + it("broadcasts progress updates when a new 10% bucket is reached", () => { + expect( + shouldBroadcastDownloadProgress( + { ...baseState, status: "downloading", downloadPercent: 19.9 }, + 20.1, + ), + ).toBe(true); + }); +}); + +describe("getAutoUpdateDisabledReason", () => { + it("reports development builds as disabled", () => { + expect( + getAutoUpdateDisabledReason({ + isDevelopment: true, + isPackaged: false, + platform: "darwin", + appImage: undefined, + disabledByEnv: false, + }), + ).toContain("packaged production builds"); + }); + + it("reports env-disabled auto updates", () => { + expect( + getAutoUpdateDisabledReason({ + isDevelopment: false, + isPackaged: true, + platform: "darwin", + appImage: undefined, + disabledByEnv: true, + }), + ).toContain("T3CODE_DISABLE_AUTO_UPDATE"); + }); + + it("reports linux non-AppImage builds as disabled", () => { + expect( + getAutoUpdateDisabledReason({ + isDevelopment: false, + isPackaged: true, + platform: "linux", + appImage: undefined, + disabledByEnv: false, + }), + ).toContain("AppImage"); + }); +}); diff --git a/apps/desktop/src/updateState.ts b/apps/desktop/src/updateState.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6291d2e5bbb62b2aed79db8831bea2a9b6788c5 --- /dev/null +++ b/apps/desktop/src/updateState.ts @@ -0,0 +1,38 @@ +import type { DesktopUpdateState } from "@t3tools/contracts"; + +export function shouldBroadcastDownloadProgress( + currentState: DesktopUpdateState, + nextPercent: number, +): boolean { + if (currentState.status !== "downloading") { + return true; + } + + const currentPercent = currentState.downloadPercent; + if (currentPercent === null) { + return true; + } + + const previousStep = Math.floor(currentPercent / 10); + const nextStep = Math.floor(nextPercent / 10); + return nextStep > previousStep || nextPercent === 100; +} + +export function getAutoUpdateDisabledReason(args: { + isDevelopment: boolean; + isPackaged: boolean; + platform: NodeJS.Platform; + appImage?: string | undefined; + disabledByEnv: boolean; +}): string | null { + if (args.isDevelopment || !args.isPackaged) { + return "Automatic updates are only available in packaged production builds."; + } + if (args.disabledByEnv) { + return "Automatic updates are disabled by the T3CODE_DISABLE_AUTO_UPDATE setting."; + } + if (args.platform === "linux" && !args.appImage) { + return "Automatic updates on Linux require running the AppImage build."; + } + return null; +}