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:
     Фонтош Иван Сергеевич
2026-05-19 11:07:19 +08:00
parent b017155eaf
commit 80103a00e7
10 changed files with 225 additions and 52 deletions
+95 -34
View File
@@ -1,4 +1,4 @@
import { app, dialog } from 'electron'; import { app, BrowserWindow, dialog } from 'electron';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import { appDisplayNameForLocale } from '../../shared/appBranding'; import { appDisplayNameForLocale } from '../../shared/appBranding';
@@ -6,6 +6,7 @@ import {
ipcChannels, ipcChannels,
type UpdaterCheckResponse, type UpdaterCheckResponse,
type UpdaterDownloadResponse, type UpdaterDownloadResponse,
type UpdaterProgressEvent,
} from '../../shared/ipc/contracts'; } from '../../shared/ipc/contracts';
import type { IpcRegisterHandler } from '../ipc/router'; import type { IpcRegisterHandler } from '../ipc/router';
import { addLicenseChangeListener } from '../license/licenseService'; import { addLicenseChangeListener } from '../license/licenseService';
@@ -55,9 +56,22 @@ function formatUpdaterError(e: unknown): string {
let lastCheckAt = 0; let lastCheckAt = 0;
/** Ручная установка: не показывать второй диалог из `update-downloaded`. */ /** Ручная установка: не показывать второй диалог из `update-downloaded`. */
let suppressAutoInstallDialog = false; let suppressAutoInstallDialog = false;
/** Версия, уже скачанная Squirrel/electron-updater (в т.ч. фоном). */
let downloadedUpdateVersion: string | null = null;
type RegisterFn = IpcRegisterHandler; 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 { function isLicensedForUpdates(licenseService: LicenseService): boolean {
const snap = licenseService.getStatusSync(); const snap = licenseService.getStatusSync();
return snap.active; return snap.active;
@@ -69,6 +83,7 @@ function maybeCheckForUpdates(licenseService: LicenseService, ignoreCooldown: bo
const now = Date.now(); const now = Date.now();
if (!ignoreCooldown && now - lastCheckAt < RE_CHECK_COOLDOWN_MS) return; if (!ignoreCooldown && now - lastCheckAt < RE_CHECK_COOLDOWN_MS) return;
lastCheckAt = now; lastCheckAt = now;
emitUpdaterProgress({ phase: 'checking' });
void autoUpdater.checkForUpdates().catch(() => undefined); void autoUpdater.checkForUpdates().catch(() => undefined);
} }
@@ -81,21 +96,29 @@ async function runManualUpdaterCheck(licenseService: LicenseService): Promise<Up
} }
const prevAutoDownload = autoUpdater.autoDownload; const prevAutoDownload = autoUpdater.autoDownload;
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
emitUpdaterProgress({ phase: 'checking' });
try { try {
const result = await autoUpdater.checkForUpdates(); const result = await autoUpdater.checkForUpdates();
if (result && result.isUpdateAvailable && result.updateInfo.version) { if (result && result.isUpdateAvailable && result.updateInfo.version) {
emitUpdaterProgress({ phase: 'available', version: result.updateInfo.version });
return { outcome: '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() }; return { outcome: 'current', currentVersion: app.getVersion() };
} catch (e) { } catch (e) {
const message = e instanceof Error ? e.message : String(e); const message = e instanceof Error ? e.message : String(e);
emitUpdaterProgress({ phase: 'error', message });
return { outcome: 'error', message }; return { outcome: 'error', message };
} finally { } finally {
autoUpdater.autoDownload = prevAutoDownload; 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) { if (!app.isPackaged) {
return { ok: false, message: 'NOT_PACKAGED' }; return { ok: false, message: 'NOT_PACKAGED' };
} }
@@ -105,25 +128,92 @@ async function runManualDownloadAndRestart(): Promise<UpdaterDownloadResponse> {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
autoUpdater.autoInstallOnAppQuit = false; autoUpdater.autoInstallOnAppQuit = false;
} }
if (!isUpdateAlreadyDownloaded(targetVersion)) {
emitUpdaterProgress({ phase: 'downloading', version: targetVersion, percent: 0 });
await withTimeout( await withTimeout(
autoUpdater.downloadUpdate(), autoUpdater.downloadUpdate(),
MANUAL_DOWNLOAD_TIMEOUT_MS, MANUAL_DOWNLOAD_TIMEOUT_MS,
'UPDATE_DOWNLOAD_TIMEOUT', 'UPDATE_DOWNLOAD_TIMEOUT',
); );
}
emitUpdaterProgress({ phase: 'installing', version: targetVersion });
quitAndInstallForPlatform(); quitAndInstallForPlatform();
return { ok: true }; return { ok: true };
} catch (e) { } catch (e) {
suppressAutoInstallDialog = false; suppressAutoInstallDialog = false;
const message = formatUpdaterError(e);
emitUpdaterProgress({ phase: 'error', message });
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
autoUpdater.autoInstallOnAppQuit = prevAutoInstallOnAppQuit; autoUpdater.autoInstallOnAppQuit = prevAutoInstallOnAppQuit;
} }
return { ok: false, message: formatUpdaterError(e) }; return { ok: false, message };
} }
} }
function registerUpdaterHandlers(register: RegisterFn, licenseService: LicenseService): void { function registerUpdaterHandlers(register: RegisterFn, licenseService: LicenseService): void {
register(ipcChannels.updater.check, () => runManualUpdaterCheck(licenseService)); 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 }); 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(); const enableDiff = process.env.DND_UPDATE_ENABLE_DIFFERENTIAL?.trim().toLowerCase();
autoUpdater.disableDifferentialDownload = !( autoUpdater.disableDifferentialDownload = !(
enableDiff === '1' || enableDiff === '1' ||
@@ -154,34 +242,7 @@ export function installAutoUpdater(licenseService: LicenseService, register: Reg
autoUpdater.autoDownload = true; autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true; autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('update-downloaded', (info) => { wireAutoUpdaterEvents(licenseService);
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);
});
setTimeout(() => { setTimeout(() => {
maybeCheckForUpdates(licenseService, true); maybeCheckForUpdates(licenseService, true);
+2 -1
View File
@@ -2,7 +2,7 @@ import path from 'node:path';
import { app, BrowserWindow } from 'electron'; import { app, BrowserWindow } from 'electron';
import { appDisplayNameForLocale } from '../../shared/appBranding'; import { appDisplayNameForLocale, windowChromeTitle } from '../../shared/appBranding';
import { getAppSemanticVersion } from '../versionInfo'; import { getAppSemanticVersion } from '../versionInfo';
import { loadBrandingWindowIcon } from './brandingIcon'; import { loadBrandingWindowIcon } from './brandingIcon';
@@ -64,6 +64,7 @@ export function createBootWindow(): BrowserWindow {
/* ignore */ /* ignore */
} }
} }
win.setTitle(windowChromeTitle('boot', app.getLocale()));
bootSplashRef = win; bootSplashRef = win;
win.once('closed', () => { win.once('closed', () => {
+3
View File
@@ -2,6 +2,7 @@ import path from 'node:path';
import { app, BrowserWindow, screen } from 'electron'; import { app, BrowserWindow, screen } from 'electron';
import { windowChromeTitle } from '../../shared/appBranding';
import { ipcChannels } from '../../shared/ipc/contracts'; import { ipcChannels } from '../../shared/ipc/contracts';
import { getBootSplashWindow } from './bootWindow'; import { getBootSplashWindow } from './bootWindow';
@@ -140,6 +141,8 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
} }
} }
win.setTitle(windowChromeTitle(kind, app.getLocale()));
win.webContents.on('preload-error', (_event, preloadPath, error) => { win.webContents.on('preload-error', (_event, preloadPath, error) => {
console.error(`[preload-error] ${preloadPath}:`, error); console.error(`[preload-error] ${preloadPath}:`, error);
}); });
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/app-window-icon.png" type="image/png" /> <link rel="icon" href="/app-window-icon.png" type="image/png" />
<title>DnD Player — Control</title> <title>TTRPG - Control</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/app-window-icon.png" type="image/png" /> <link rel="icon" href="/app-window-icon.png" type="image/png" />
<title>DnD Player — Editor</title> <title>TTRPG - Editor</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+64 -8
View File
@@ -1,7 +1,11 @@
import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { ipcChannels, type UpdaterCheckResponse } from '../../shared/ipc/contracts'; import {
ipcChannels,
type UpdaterCheckResponse,
type UpdaterProgressEvent,
} from '../../shared/ipc/contracts';
import { EULA_CURRENT_VERSION } from '../../shared/license/eulaVersion'; import { EULA_CURRENT_VERSION } from '../../shared/license/eulaVersion';
import type { LicenseSnapshot } from '../../shared/license/licenseSnapshot'; import type { LicenseSnapshot } from '../../shared/license/licenseSnapshot';
import { PROJECT_ZIP_EXTENSION } from '../../shared/project/projectZipExtension'; import { PROJECT_ZIP_EXTENSION } from '../../shared/project/projectZipExtension';
@@ -965,28 +969,73 @@ type CheckUpdatesModalProps = {
onClose: () => void; onClose: () => void;
}; };
function formatUpdaterStageLabel(
t: (key: string, vars?: Record<string, string | number>) => string,
ev: UpdaterProgressEvent | null,
): string {
if (!ev) return t('updates.stage.checking');
const percentSuffix =
ev.phase === 'downloading' && ev.percent !== undefined
? t('updates.stagePercent', { percent: ev.percent })
: '';
switch (ev.phase) {
case 'checking':
return t('updates.stage.checking');
case 'available':
return t('updates.stage.available', { version: ev.version ?? '?' });
case 'not-available':
return t('updates.stage.not-available');
case 'downloading':
return t('updates.stage.downloading', { percent: percentSuffix });
case 'installing':
return t('updates.stage.installing');
case 'error':
return ev.message ? `${t('updates.stage.error')}: ${ev.message}` : t('updates.stage.error');
default:
return t('updates.stage.checking');
}
}
function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) { function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
const { t } = useEditorI18n(); const { t } = useEditorI18n();
const [phase, setPhase] = useState<'idle' | 'checking' | 'done'>('idle'); const [phase, setPhase] = useState<'idle' | 'checking' | 'done'>('idle');
const [res, setRes] = useState<UpdaterCheckResponse | null>(null); const [res, setRes] = useState<UpdaterCheckResponse | null>(null);
const [downloadBusy, setDownloadBusy] = useState(false); const [downloadBusy, setDownloadBusy] = useState(false);
const [progress, setProgress] = useState<UpdaterProgressEvent | null>(null);
useEffect(() => { useEffect(() => {
if (!open) return; if (!open) return;
startTransition(() => { startTransition(() => {
setPhase('checking'); setPhase('checking');
setRes(null); setRes(null);
setProgress({ phase: 'checking' });
setDownloadBusy(false);
}); });
void getDndApi() void getDndApi()
.invoke(ipcChannels.updater.check, {}) .invoke(ipcChannels.updater.check, {})
.then((r) => { .then((r) => {
setRes(r); setRes(r);
setPhase('done'); setPhase('done');
if (r.outcome === 'available') {
setProgress({ phase: 'available', version: r.version });
} else if (r.outcome === 'current') {
setProgress({ phase: 'not-available', version: r.currentVersion });
} else if (r.outcome === 'error') {
setProgress({ phase: 'error', message: r.message });
}
}) })
.catch((e: unknown) => { .catch((e: unknown) => {
const message = e instanceof Error ? e.message : String(e); const message = e instanceof Error ? e.message : String(e);
setRes({ outcome: 'error', message }); setRes({ outcome: 'error', message });
setPhase('done'); setPhase('done');
setProgress({ phase: 'error', message });
});
}, [open]);
useEffect(() => {
if (!open) return;
return getDndApi().on(ipcChannels.updater.progress, (ev) => {
setProgress(ev);
}); });
}, [open]); }, [open]);
@@ -1001,6 +1050,8 @@ function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
if (!open) return null; if (!open) return null;
const stageLine = t('updates.stageLine', { stage: formatUpdaterStageLabel(t, progress) });
const body = const body =
phase === 'checking' || res === null ? ( phase === 'checking' || res === null ? (
<div className={styles.muted}>{t('updates.checking')}</div> <div className={styles.muted}>{t('updates.checking')}</div>
@@ -1042,7 +1093,10 @@ function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
× ×
</button> </button>
</div> </div>
<div className={styles.fieldGrid}>{body}</div> <div className={styles.fieldGrid}>
{body}
<div className={styles.muted}>{stageLine}</div>
</div>
<div className={styles.modalFooter}> <div className={styles.modalFooter}>
{showUpdateIdle ? ( {showUpdateIdle ? (
<> <>
@@ -1053,21 +1107,23 @@ function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
variant="primary" variant="primary"
disabled={downloadBusy} disabled={downloadBusy}
onClick={() => { onClick={() => {
if (res?.outcome !== 'available') return;
setDownloadBusy(true); setDownloadBusy(true);
setProgress({ phase: 'downloading', version: res.version, percent: 0 });
void getDndApi() void getDndApi()
.invoke(ipcChannels.updater.downloadAndRestart, {}) .invoke(ipcChannels.updater.downloadAndRestart, { version: res.version })
.then((r) => { .then((r) => {
if (!r.ok) { if (!r.ok) {
setDownloadBusy(false); setDownloadBusy(false);
setRes({ outcome: 'error', message: r.message }); setRes({ outcome: 'error', message: r.message });
setProgress({ phase: 'error', message: r.message });
} }
}) })
.catch((e: unknown) => { .catch((e: unknown) => {
const message = e instanceof Error ? e.message : String(e);
setDownloadBusy(false); setDownloadBusy(false);
setRes({ setRes({ outcome: 'error', message });
outcome: 'error', setProgress({ phase: 'error', message });
message: e instanceof Error ? e.message : String(e),
});
}); });
}} }}
> >
@@ -1076,7 +1132,7 @@ function CheckUpdatesModal({ open, onClose }: CheckUpdatesModalProps) {
</> </>
) : showUpdateBusy ? ( ) : showUpdateBusy ? (
<Button variant="primary" disabled> <Button variant="primary" disabled>
{t('updates.downloading')} {progress?.phase === 'installing' ? t('updates.stage.installing') : t('updates.downloading')}
</Button> </Button>
) : ( ) : (
<Button variant="primary" disabled={downloadBusy} onClick={onClose}> <Button variant="primary" disabled={downloadBusy} onClick={onClose}>
@@ -130,6 +130,14 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
'updates.noLicense': 'Нужна активная лицензия.', 'updates.noLicense': 'Нужна активная лицензия.',
'updates.download': 'Обновить', 'updates.download': 'Обновить',
'updates.downloading': 'Загрузка…', 'updates.downloading': 'Загрузка…',
'updates.stageLine': 'Этап: {stage}',
'updates.stage.checking': 'проверка обновлений',
'updates.stage.available': 'доступна версия {version}',
'updates.stage.not-available': 'актуальная версия',
'updates.stage.downloading': 'загрузка{percent}',
'updates.stage.installing': 'установка и перезапуск',
'updates.stage.error': 'ошибка',
'updates.stagePercent': ' ({percent}%)',
'projectMenu.home': 'Начальный экран', 'projectMenu.home': 'Начальный экран',
'projectMenu.import': 'Импорт', 'projectMenu.import': 'Импорт',
@@ -359,6 +367,14 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
'updates.noLicense': 'An active license is required.', 'updates.noLicense': 'An active license is required.',
'updates.download': 'Update', 'updates.download': 'Update',
'updates.downloading': 'Downloading…', 'updates.downloading': 'Downloading…',
'updates.stageLine': 'Stage: {stage}',
'updates.stage.checking': 'checking for updates',
'updates.stage.available': 'version {version} available',
'updates.stage.not-available': 'up to date',
'updates.stage.downloading': 'downloading{percent}',
'updates.stage.installing': 'installing and restarting',
'updates.stage.error': 'error',
'updates.stagePercent': ' ({percent}%)',
'projectMenu.home': 'Home', 'projectMenu.home': 'Home',
'projectMenu.import': 'Import', 'projectMenu.import': 'Import',
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/app-window-icon.png" type="image/png" /> <link rel="icon" href="/app-window-icon.png" type="image/png" />
<title>DnD Player — Presentation</title> <title>TTRPG - Presentation</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+18
View File
@@ -11,3 +11,21 @@ export function appDisplayNameForLocale(localeTag: string): string {
if (tag.startsWith('ru')) return APP_DISPLAY_NAME_RU; if (tag.startsWith('ru')) return APP_DISPLAY_NAME_RU;
return APP_DISPLAY_NAME_EN; return APP_DISPLAY_NAME_EN;
} }
/** Префикс заголовка окон: `TTRPG - Редактор`. */
export const APP_WINDOW_BRAND = 'TTRPG';
export type AppWindowKind = 'editor' | 'presentation' | 'control' | 'boot';
const WINDOW_SUFFIX: Record<AppWindowKind, { ru: string; en: string }> = {
editor: { ru: 'Редактор', en: 'Editor' },
presentation: { ru: 'Презентация', en: 'Presentation' },
control: { ru: 'Пульт', en: 'Control' },
boot: { ru: 'Загрузка', en: 'Loading' },
};
export function windowChromeTitle(kind: AppWindowKind, localeTag: string): string {
const tag = localeTag.trim().toLowerCase();
const suffix = tag.startsWith('ru') ? WINDOW_SUFFIX[kind].ru : WINDOW_SUFFIX[kind].en;
return `${APP_WINDOW_BRAND} - ${suffix}`;
}
+19 -1
View File
@@ -21,6 +21,7 @@ export const ipcChannels = {
updater: { updater: {
check: 'updater.check', check: 'updater.check',
downloadAndRestart: 'updater.downloadAndRestart', downloadAndRestart: 'updater.downloadAndRestart',
progress: 'updater.progress',
}, },
project: { project: {
list: 'project.list', list: 'project.list',
@@ -97,6 +98,22 @@ export type UpdaterCheckResponse =
export type UpdaterDownloadResponse = { ok: true } | { ok: false; message: string }; export type UpdaterDownloadResponse = { ok: true } | { ok: false; message: string };
export type UpdaterProgressPhase =
| 'checking'
| 'available'
| 'not-available'
| 'downloading'
| 'installing'
| 'error';
export type UpdaterProgressEvent = {
phase: UpdaterProgressPhase;
/** 0..100 при phase=downloading */
percent?: number;
version?: string;
message?: string;
};
export type IpcEventMap = { export type IpcEventMap = {
[ipcChannels.session.stateChanged]: { state: SessionState }; [ipcChannels.session.stateChanged]: { state: SessionState };
[ipcChannels.effects.stateChanged]: { state: EffectsState }; [ipcChannels.effects.stateChanged]: { state: EffectsState };
@@ -105,6 +122,7 @@ export type IpcEventMap = {
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean }; [ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
[ipcChannels.project.importZipProgress]: ZipProgressEvent; [ipcChannels.project.importZipProgress]: ZipProgressEvent;
[ipcChannels.project.exportZipProgress]: ZipProgressEvent; [ipcChannels.project.exportZipProgress]: ZipProgressEvent;
[ipcChannels.updater.progress]: UpdaterProgressEvent;
}; };
export type IpcInvokeMap = { export type IpcInvokeMap = {
@@ -121,7 +139,7 @@ export type IpcInvokeMap = {
res: UpdaterCheckResponse; res: UpdaterCheckResponse;
}; };
[ipcChannels.updater.downloadAndRestart]: { [ipcChannels.updater.downloadAndRestart]: {
req: Record<string, never>; req: { version: string };
res: UpdaterDownloadResponse; res: UpdaterDownloadResponse;
}; };
[ipcChannels.project.list]: { [ipcChannels.project.list]: {