fix(mac): updater progress UI and skip redundant download
Show update stage in the modal, forward electron-updater progress over IPC, and install immediately when the build is already cached. Rename window titles to TTRPG - *. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { app, dialog } from 'electron';
|
||||
import { app, BrowserWindow, dialog } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
|
||||
import { appDisplayNameForLocale } from '../../shared/appBranding';
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ipcChannels,
|
||||
type UpdaterCheckResponse,
|
||||
type UpdaterDownloadResponse,
|
||||
type UpdaterProgressEvent,
|
||||
} from '../../shared/ipc/contracts';
|
||||
import type { IpcRegisterHandler } from '../ipc/router';
|
||||
import { addLicenseChangeListener } from '../license/licenseService';
|
||||
@@ -55,9 +56,22 @@ function formatUpdaterError(e: unknown): string {
|
||||
let lastCheckAt = 0;
|
||||
/** Ручная установка: не показывать второй диалог из `update-downloaded`. */
|
||||
let suppressAutoInstallDialog = false;
|
||||
/** Версия, уже скачанная Squirrel/electron-updater (в т.ч. фоном). */
|
||||
let downloadedUpdateVersion: string | null = null;
|
||||
|
||||
type RegisterFn = IpcRegisterHandler;
|
||||
|
||||
function emitUpdaterProgress(ev: UpdaterProgressEvent): void {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
if (win.isDestroyed() || win.webContents.isDestroyed()) continue;
|
||||
try {
|
||||
win.webContents.send(ipcChannels.updater.progress, ev);
|
||||
} catch {
|
||||
/* окно закрылось */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isLicensedForUpdates(licenseService: LicenseService): boolean {
|
||||
const snap = licenseService.getStatusSync();
|
||||
return snap.active;
|
||||
@@ -69,6 +83,7 @@ function maybeCheckForUpdates(licenseService: LicenseService, ignoreCooldown: bo
|
||||
const now = Date.now();
|
||||
if (!ignoreCooldown && now - lastCheckAt < RE_CHECK_COOLDOWN_MS) return;
|
||||
lastCheckAt = now;
|
||||
emitUpdaterProgress({ phase: 'checking' });
|
||||
void autoUpdater.checkForUpdates().catch(() => undefined);
|
||||
}
|
||||
|
||||
@@ -81,21 +96,29 @@ async function runManualUpdaterCheck(licenseService: LicenseService): Promise<Up
|
||||
}
|
||||
const prevAutoDownload = autoUpdater.autoDownload;
|
||||
autoUpdater.autoDownload = false;
|
||||
emitUpdaterProgress({ phase: 'checking' });
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
if (result && result.isUpdateAvailable && result.updateInfo.version) {
|
||||
emitUpdaterProgress({ phase: 'available', version: result.updateInfo.version });
|
||||
return { outcome: 'available', version: result.updateInfo.version };
|
||||
}
|
||||
emitUpdaterProgress({ phase: 'not-available', version: app.getVersion() });
|
||||
return { outcome: 'current', currentVersion: app.getVersion() };
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
emitUpdaterProgress({ phase: 'error', message });
|
||||
return { outcome: 'error', message };
|
||||
} finally {
|
||||
autoUpdater.autoDownload = prevAutoDownload;
|
||||
}
|
||||
}
|
||||
|
||||
async function runManualDownloadAndRestart(): Promise<UpdaterDownloadResponse> {
|
||||
function isUpdateAlreadyDownloaded(targetVersion: string): boolean {
|
||||
return downloadedUpdateVersion !== null && downloadedUpdateVersion === targetVersion;
|
||||
}
|
||||
|
||||
async function runManualDownloadAndRestart(targetVersion: string): Promise<UpdaterDownloadResponse> {
|
||||
if (!app.isPackaged) {
|
||||
return { ok: false, message: 'NOT_PACKAGED' };
|
||||
}
|
||||
@@ -105,25 +128,92 @@ async function runManualDownloadAndRestart(): Promise<UpdaterDownloadResponse> {
|
||||
if (process.platform === 'darwin') {
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
}
|
||||
await withTimeout(
|
||||
autoUpdater.downloadUpdate(),
|
||||
MANUAL_DOWNLOAD_TIMEOUT_MS,
|
||||
'UPDATE_DOWNLOAD_TIMEOUT',
|
||||
);
|
||||
|
||||
if (!isUpdateAlreadyDownloaded(targetVersion)) {
|
||||
emitUpdaterProgress({ phase: 'downloading', version: targetVersion, percent: 0 });
|
||||
await withTimeout(
|
||||
autoUpdater.downloadUpdate(),
|
||||
MANUAL_DOWNLOAD_TIMEOUT_MS,
|
||||
'UPDATE_DOWNLOAD_TIMEOUT',
|
||||
);
|
||||
}
|
||||
|
||||
emitUpdaterProgress({ phase: 'installing', version: targetVersion });
|
||||
quitAndInstallForPlatform();
|
||||
return { ok: true };
|
||||
} catch (e) {
|
||||
suppressAutoInstallDialog = false;
|
||||
const message = formatUpdaterError(e);
|
||||
emitUpdaterProgress({ phase: 'error', message });
|
||||
if (process.platform === 'darwin') {
|
||||
autoUpdater.autoInstallOnAppQuit = prevAutoInstallOnAppQuit;
|
||||
}
|
||||
return { ok: false, message: formatUpdaterError(e) };
|
||||
return { ok: false, message };
|
||||
}
|
||||
}
|
||||
|
||||
function registerUpdaterHandlers(register: RegisterFn, licenseService: LicenseService): void {
|
||||
register(ipcChannels.updater.check, () => runManualUpdaterCheck(licenseService));
|
||||
register(ipcChannels.updater.downloadAndRestart, () => runManualDownloadAndRestart());
|
||||
register(ipcChannels.updater.downloadAndRestart, (req: { version: string }) =>
|
||||
runManualDownloadAndRestart(req.version),
|
||||
);
|
||||
}
|
||||
|
||||
function wireAutoUpdaterEvents(licenseService: LicenseService): void {
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
emitUpdaterProgress({ phase: 'checking' });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', (info) => {
|
||||
emitUpdaterProgress({ phase: 'available', version: info.version });
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', (info) => {
|
||||
emitUpdaterProgress({ phase: 'not-available', version: info.version });
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progress) => {
|
||||
const percent =
|
||||
typeof progress.percent === 'number' && Number.isFinite(progress.percent)
|
||||
? Math.round(progress.percent)
|
||||
: undefined;
|
||||
const ev: UpdaterProgressEvent = { phase: 'downloading' };
|
||||
if (percent !== undefined) ev.percent = percent;
|
||||
emitUpdaterProgress(ev);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
downloadedUpdateVersion = info.version;
|
||||
emitUpdaterProgress({ phase: 'downloading', version: info.version, percent: 100 });
|
||||
if (suppressAutoInstallDialog) {
|
||||
suppressAutoInstallDialog = false;
|
||||
return;
|
||||
}
|
||||
void dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: appDisplayNameForLocale(app.getLocale()),
|
||||
message: `Доступна новая версия ${info.version}. Установить и перезапустить?`,
|
||||
buttons: ['Перезапустить сейчас', 'Позже'],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.response === 0) {
|
||||
emitUpdaterProgress({ phase: 'installing', version: info.version });
|
||||
quitAndInstallForPlatform();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (e) => {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
emitUpdaterProgress({ phase: 'error', message });
|
||||
});
|
||||
|
||||
addLicenseChangeListener(() => {
|
||||
maybeCheckForUpdates(licenseService, false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,8 +232,6 @@ export function installAutoUpdater(licenseService: LicenseService, register: Reg
|
||||
autoUpdater.setFeedURL({ provider: 'generic', url });
|
||||
}
|
||||
|
||||
// Дифференциальное обновление (multi-Range по blockmap) часто ломается на Gitea raw за nginx (HTTP 400);
|
||||
// electron-updater тогда и так падает на полный файл — отключаем лишний шум и лишний round-trip.
|
||||
const enableDiff = process.env.DND_UPDATE_ENABLE_DIFFERENTIAL?.trim().toLowerCase();
|
||||
autoUpdater.disableDifferentialDownload = !(
|
||||
enableDiff === '1' ||
|
||||
@@ -154,34 +242,7 @@ export function installAutoUpdater(licenseService: LicenseService, register: Reg
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
if (suppressAutoInstallDialog) {
|
||||
suppressAutoInstallDialog = false;
|
||||
return;
|
||||
}
|
||||
void dialog
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: appDisplayNameForLocale(app.getLocale()),
|
||||
message: `Доступна новая версия ${info.version}. Установить и перезапустить?`,
|
||||
buttons: ['Перезапустить сейчас', 'Позже'],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.response === 0) {
|
||||
quitAndInstallForPlatform();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
autoUpdater.on('error', () => {
|
||||
/* без console: в production main минифицируется с drop console */
|
||||
});
|
||||
|
||||
addLicenseChangeListener(() => {
|
||||
maybeCheckForUpdates(licenseService, false);
|
||||
});
|
||||
wireAutoUpdaterEvents(licenseService);
|
||||
|
||||
setTimeout(() => {
|
||||
maybeCheckForUpdates(licenseService, true);
|
||||
|
||||
Reference in New Issue
Block a user