diff --git a/src/main/ipc.ts b/src/main/ipc.ts index d6270dc..99fbfc1 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -285,8 +285,13 @@ export function registerIpc(): void { // Auto-updater ipcMain.handle(IPC.updaterStatus, () => getUpdaterStatus()) ipcMain.handle(IPC.updaterCheck, () => checkForUpdates()) - ipcMain.handle(IPC.updaterDownload, () => downloadUpdate()) - ipcMain.handle(IPC.updaterInstall, () => quitAndInstall()) + // download/install — fire-and-forget. Прогресс и завершение приходят в + // renderer через evtUpdaterStatus, ждать promise бессмысленно — renderer + // только зря держал бы `busy=true` весь download (минуты на медленной сети). + ipcMain.on(IPC.updaterDownload, () => { + void downloadUpdate() + }) + ipcMain.on(IPC.updaterInstall, () => quitAndInstall()) // History ipcMain.handle(IPC.getHistory, (_e, sinceMs?: number) => getHistory(sinceMs)) diff --git a/src/main/updater.ts b/src/main/updater.ts index 4878ecc..057d979 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -172,5 +172,12 @@ export async function downloadUpdate(): Promise { export function quitAndInstall(): void { if (!app.isPackaged) return - autoUpdater.quitAndInstall() + // (isSilent=true, isForceRunAfter=true): + // - isSilent: NSIS работает без UI-диалогов установки → restart занимает + // ~1-2 сек вместо ~5-10 (без чёрного окна установщика на половину экрана). + // - isForceRunAfter: гарантируем что после установки приложение запустится + // автоматически, даже если в NSIS-конфиге runAfterFinish был выключен + // для этого сценария. Без этого пользователь нажал «Рестарт» — и остался + // без открытого приложения. + autoUpdater.quitAndInstall(true, true) } diff --git a/src/preload/index.ts b/src/preload/index.ts index e0c6140..7e13505 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -105,8 +105,10 @@ const api = { ipcRenderer.invoke(IPC.updaterStatus), updaterCheck: (): Promise => ipcRenderer.invoke(IPC.updaterCheck), - updaterDownload: (): Promise => ipcRenderer.invoke(IPC.updaterDownload), - updaterInstall: (): Promise => ipcRenderer.invoke(IPC.updaterInstall), + // Fire-and-forget. Прогресс и завершение прилетают через onUpdaterStatus — + // renderer не должен `await`'ить, иначе busy-state висит весь download. + updaterDownload: (): void => ipcRenderer.send(IPC.updaterDownload), + updaterInstall: (): void => ipcRenderer.send(IPC.updaterInstall), // History getHistory: (sinceMs?: number): Promise => diff --git a/src/renderer/src/components/UpdaterCard.tsx b/src/renderer/src/components/UpdaterCard.tsx index f4ef01b..c9c1471 100644 --- a/src/renderer/src/components/UpdaterCard.tsx +++ b/src/renderer/src/components/UpdaterCard.tsx @@ -24,6 +24,9 @@ function formatChecked(ts: number, t: TFn): string { export function UpdaterCard(): JSX.Element { const [status, setStatus] = useState({ kind: 'idle' }) + // busy используется только для синхронного `check()` — для асинхронного + // download/install статус сам переключится через события (downloading → + // downloaded), отдельный busy-флаг будет только дублировать визуально. const [busy, setBusy] = useState(false) useEffect(() => { @@ -39,16 +42,15 @@ export function UpdaterCard(): JSX.Element { setBusy(false) } } - async function download(): Promise { - setBusy(true) - try { - await window.api.updaterDownload() - } finally { - setBusy(false) - } + function download(): void { + // Fire-and-forget — UI моментально перейдёт в kind:'downloading' через + // первое же event'ное обновление статуса. Никакого `await` — пользователь + // должен иметь возможность уйти на Dashboard, продолжать упражнения, + // пока обновление качается в фоне. + window.api.updaterDownload() } function install(): void { - void window.api.updaterInstall() + window.api.updaterInstall() } return ( @@ -180,6 +182,10 @@ function Body({ transition={{ duration: 0.3, ease: 'linear' }} /> + {/* Подсказка: download идёт в фоне, не нужно сидеть на этом экране. */} +
+ {t('updater.downloading.hint')} +
) } diff --git a/src/renderer/src/i18n/dict.ts b/src/renderer/src/i18n/dict.ts index 1c397f4..062aeb5 100644 --- a/src/renderer/src/i18n/dict.ts +++ b/src/renderer/src/i18n/dict.ts @@ -194,8 +194,9 @@ export const ru: Dict = { 'updater.available.title': 'Доступна v{v}', 'updater.downloading.title': 'Загружаем обновление', 'updater.downloading.subtitle': '{got} / {total} МБ · {speed} МБ/с', + 'updater.downloading.hint': 'Можно закрыть это окно — скачивание продолжится в фоне.', 'updater.downloaded.title': 'Готово · v{v}', - 'updater.downloaded.subtitle': 'Перезапусти для применения', + 'updater.downloaded.subtitle': 'Нажми «Рестарт» — приложение моментально откроется в новой версии.', 'updater.error.title': 'Ошибка проверки', 'updater.idle.title': 'Проверить обновления', 'updater.idle.subtitle': 'Авто-проверка раз в час', @@ -440,8 +441,9 @@ export const en: Dict = { 'updater.available.title': 'v{v} available', 'updater.downloading.title': 'Downloading update', 'updater.downloading.subtitle': '{got} / {total} MB · {speed} MB/s', + 'updater.downloading.hint': 'You can close this window — download continues in the background.', 'updater.downloaded.title': 'Ready · v{v}', - 'updater.downloaded.subtitle': 'Restart to apply', + 'updater.downloaded.subtitle': 'Click Restart — the app will reopen instantly in the new version.', 'updater.error.title': 'Check failed', 'updater.idle.title': 'Check for updates', 'updater.idle.subtitle': 'Auto-check every hour',