feat(updater): фоновое скачивание + моментальный рестарт
Раньше после «Скачать» 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` уже был включён — если пользователь
не нажал «Рестарт» и просто закрыл приложение, установка
произойдёт при следующем закрытии автоматически.
This commit is contained in:
@@ -285,8 +285,13 @@ export function registerIpc(): void {
|
|||||||
// Auto-updater
|
// Auto-updater
|
||||||
ipcMain.handle(IPC.updaterStatus, () => getUpdaterStatus())
|
ipcMain.handle(IPC.updaterStatus, () => getUpdaterStatus())
|
||||||
ipcMain.handle(IPC.updaterCheck, () => checkForUpdates())
|
ipcMain.handle(IPC.updaterCheck, () => checkForUpdates())
|
||||||
ipcMain.handle(IPC.updaterDownload, () => downloadUpdate())
|
// download/install — fire-and-forget. Прогресс и завершение приходят в
|
||||||
ipcMain.handle(IPC.updaterInstall, () => quitAndInstall())
|
// renderer через evtUpdaterStatus, ждать promise бессмысленно — renderer
|
||||||
|
// только зря держал бы `busy=true` весь download (минуты на медленной сети).
|
||||||
|
ipcMain.on(IPC.updaterDownload, () => {
|
||||||
|
void downloadUpdate()
|
||||||
|
})
|
||||||
|
ipcMain.on(IPC.updaterInstall, () => quitAndInstall())
|
||||||
|
|
||||||
// History
|
// History
|
||||||
ipcMain.handle(IPC.getHistory, (_e, sinceMs?: number) => getHistory(sinceMs))
|
ipcMain.handle(IPC.getHistory, (_e, sinceMs?: number) => getHistory(sinceMs))
|
||||||
|
|||||||
@@ -172,5 +172,12 @@ export async function downloadUpdate(): Promise<void> {
|
|||||||
|
|
||||||
export function quitAndInstall(): void {
|
export function quitAndInstall(): void {
|
||||||
if (!app.isPackaged) return
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,8 +105,10 @@ const api = {
|
|||||||
ipcRenderer.invoke(IPC.updaterStatus),
|
ipcRenderer.invoke(IPC.updaterStatus),
|
||||||
updaterCheck: (): Promise<UpdaterStatus> =>
|
updaterCheck: (): Promise<UpdaterStatus> =>
|
||||||
ipcRenderer.invoke(IPC.updaterCheck),
|
ipcRenderer.invoke(IPC.updaterCheck),
|
||||||
updaterDownload: (): Promise<void> => ipcRenderer.invoke(IPC.updaterDownload),
|
// Fire-and-forget. Прогресс и завершение прилетают через onUpdaterStatus —
|
||||||
updaterInstall: (): Promise<void> => ipcRenderer.invoke(IPC.updaterInstall),
|
// renderer не должен `await`'ить, иначе busy-state висит весь download.
|
||||||
|
updaterDownload: (): void => ipcRenderer.send(IPC.updaterDownload),
|
||||||
|
updaterInstall: (): void => ipcRenderer.send(IPC.updaterInstall),
|
||||||
|
|
||||||
// History
|
// History
|
||||||
getHistory: (sinceMs?: number): Promise<HistoryEntry[]> =>
|
getHistory: (sinceMs?: number): Promise<HistoryEntry[]> =>
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ function formatChecked(ts: number, t: TFn): string {
|
|||||||
|
|
||||||
export function UpdaterCard(): JSX.Element {
|
export function UpdaterCard(): JSX.Element {
|
||||||
const [status, setStatus] = useState<UpdaterStatus>({ kind: 'idle' })
|
const [status, setStatus] = useState<UpdaterStatus>({ kind: 'idle' })
|
||||||
|
// busy используется только для синхронного `check()` — для асинхронного
|
||||||
|
// download/install статус сам переключится через события (downloading →
|
||||||
|
// downloaded), отдельный busy-флаг будет только дублировать визуально.
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,16 +42,15 @@ export function UpdaterCard(): JSX.Element {
|
|||||||
setBusy(false)
|
setBusy(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function download(): Promise<void> {
|
function download(): void {
|
||||||
setBusy(true)
|
// Fire-and-forget — UI моментально перейдёт в kind:'downloading' через
|
||||||
try {
|
// первое же event'ное обновление статуса. Никакого `await` — пользователь
|
||||||
await window.api.updaterDownload()
|
// должен иметь возможность уйти на Dashboard, продолжать упражнения,
|
||||||
} finally {
|
// пока обновление качается в фоне.
|
||||||
setBusy(false)
|
window.api.updaterDownload()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function install(): void {
|
function install(): void {
|
||||||
void window.api.updaterInstall()
|
window.api.updaterInstall()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -180,6 +182,10 @@ function Body({
|
|||||||
transition={{ duration: 0.3, ease: 'linear' }}
|
transition={{ duration: 0.3, ease: 'linear' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Подсказка: download идёт в фоне, не нужно сидеть на этом экране. */}
|
||||||
|
<div className="text-[12px] text-text/55 mt-3 font-medium">
|
||||||
|
{t('updater.downloading.hint')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,8 +194,9 @@ export const ru: Dict = {
|
|||||||
'updater.available.title': 'Доступна v{v}',
|
'updater.available.title': 'Доступна v{v}',
|
||||||
'updater.downloading.title': 'Загружаем обновление',
|
'updater.downloading.title': 'Загружаем обновление',
|
||||||
'updater.downloading.subtitle': '{got} / {total} МБ · {speed} МБ/с',
|
'updater.downloading.subtitle': '{got} / {total} МБ · {speed} МБ/с',
|
||||||
|
'updater.downloading.hint': 'Можно закрыть это окно — скачивание продолжится в фоне.',
|
||||||
'updater.downloaded.title': 'Готово · v{v}',
|
'updater.downloaded.title': 'Готово · v{v}',
|
||||||
'updater.downloaded.subtitle': 'Перезапусти для применения',
|
'updater.downloaded.subtitle': 'Нажми «Рестарт» — приложение моментально откроется в новой версии.',
|
||||||
'updater.error.title': 'Ошибка проверки',
|
'updater.error.title': 'Ошибка проверки',
|
||||||
'updater.idle.title': 'Проверить обновления',
|
'updater.idle.title': 'Проверить обновления',
|
||||||
'updater.idle.subtitle': 'Авто-проверка раз в час',
|
'updater.idle.subtitle': 'Авто-проверка раз в час',
|
||||||
@@ -440,8 +441,9 @@ export const en: Dict = {
|
|||||||
'updater.available.title': 'v{v} available',
|
'updater.available.title': 'v{v} available',
|
||||||
'updater.downloading.title': 'Downloading update',
|
'updater.downloading.title': 'Downloading update',
|
||||||
'updater.downloading.subtitle': '{got} / {total} MB · {speed} MB/s',
|
'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.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.error.title': 'Check failed',
|
||||||
'updater.idle.title': 'Check for updates',
|
'updater.idle.title': 'Check for updates',
|
||||||
'updater.idle.subtitle': 'Auto-check every hour',
|
'updater.idle.subtitle': 'Auto-check every hour',
|
||||||
|
|||||||
Reference in New Issue
Block a user