From c5c05ee6516fe74987b738f63eff100889da9e2e Mon Sep 17 00:00:00 2001 From: AnRil Date: Tue, 19 May 2026 21:33:00 +0700 Subject: [PATCH] =?UTF-8?q?feat(updater):=20=D1=84=D0=BE=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=B5=20=D1=81=D0=BA=D0=B0=D1=87=D0=B8=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20+=20=D0=BC=D0=BE=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D1=80=D0=B5=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше после «Скачать» renderer ждал promise (`ipcRenderer.invoke`), пока electron-updater не завершит весь download. Если пользователь закрывал Settings и уходил на Dashboard — скачивание продолжалось, но кнопка возвращалась в `busy=true` при следующем открытии. Сама установка через `quitAndInstall()` без параметров поднимала NSIS-диалог установщика — ~5-10 сек до запуска новой версии. Что изменилось: - IPC `updaterDownload` / `updaterInstall` — fire-and-forget через `ipcMain.on` / `ipcRenderer.send`. Renderer триггерит и забывает, прогресс приходит через `evtUpdaterStatus`. UI моментально переключается в kind:'downloading' и не блокируется ожиданием. - `autoUpdater.quitAndInstall(true, true)`: - isSilent=true: NSIS работает без UI установщика (~1-2 сек вместо ~5-10), без чёрного окна на половину экрана. - isForceRunAfter=true: гарантия что приложение запустится после установки (иначе пользователь нажал «Рестарт» и остался без открытого приложения). - UpdaterCard: убран `busy` для async download — статус сам переключается через события. Добавлена подсказка «можно закрыть это окно, скачивание продолжится в фоне». Подкручен subtitle на downloaded-state: «нажми Рестарт — приложение моментально откроется в новой версии». - i18n: новый ключ `updater.downloading.hint` (RU + EN), обновлён `updater.downloaded.subtitle`. `autoInstallOnAppQuit = true` уже был включён — если пользователь не нажал «Рестарт» и просто закрыл приложение, установка произойдёт при следующем закрытии автоматически. --- src/main/ipc.ts | 9 +++++++-- src/main/updater.ts | 9 ++++++++- src/preload/index.ts | 6 ++++-- src/renderer/src/components/UpdaterCard.tsx | 22 +++++++++++++-------- src/renderer/src/i18n/dict.ts | 6 ++++-- 5 files changed, 37 insertions(+), 15 deletions(-) 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',