From a2d81e6204199954901e92e6a4eff4a9d9c8bf6e Mon Sep 17 00:00:00 2001 From: beyondkmp Date: Thu, 9 Oct 2025 15:58:24 +0800 Subject: [PATCH] feat: add updating dialog in render (#10569) * feat: replace update dialog handling with quit and install functionality * refactor: remove App_ShowUpdateDialog and implement App_QuitAndInstall in IpcChannel * update ipc.ts to handle quit and install action * modify AppUpdater to include quitAndInstall method * adjust preload index to invoke new quit and install action * enhance AboutSettings to manage update dialog state and trigger quit and install * fix(AboutSettings): handle null update info in update dialog state management * fix(UpdateDialog): improve error handling during update installation and enhance release notes processing * fix(AppUpdater): remove redundant assignment of releaseInfo after update download * fix(IpcChannel): remove UpdateDownloadedCancelled enum value * format code * fix(UpdateDialog): enhance installation process with loading state and error handling * update i18n * fix(i18n): Auto update translations for PR #10569 * feat(UpdateAppButton): integrate UpdateDialog and update button functionality for better user experience * fix(UpdateDialog): update installation handler to support async operation and ensure modal closes after installation * refactor(AppUpdater.test): remove deprecated formatReleaseNotes tests to streamline test suite * refactor(update-dialog): simplify dialog close handling Replace onOpenChange with onClose prop to directly handle dialog closing Remove redundant handleClose function and simplify button onPress handler --------- Co-authored-by: GitHub Action Co-authored-by: icarus --- packages/shared/IpcChannel.ts | 3 +- src/main/ipc.ts | 2 +- src/main/services/AppUpdater.ts | 69 +----------- .../services/__tests__/AppUpdater.test.ts | 42 -------- src/preload/index.ts | 2 +- src/renderer/src/components/UpdateDialog.tsx | 101 ++++++++++++++++++ src/renderer/src/i18n/locales/en-us.json | 1 + src/renderer/src/i18n/locales/zh-cn.json | 1 + src/renderer/src/i18n/locales/zh-tw.json | 1 + src/renderer/src/i18n/translate/el-gr.json | 1 + src/renderer/src/i18n/translate/es-es.json | 1 + src/renderer/src/i18n/translate/fr-fr.json | 1 + src/renderer/src/i18n/translate/ja-jp.json | 1 + src/renderer/src/i18n/translate/pt-pt.json | 1 + src/renderer/src/i18n/translate/ru-ru.json | 1 + .../pages/home/components/UpdateAppButton.tsx | 7 +- .../src/pages/settings/AboutSettings.tsx | 15 ++- 17 files changed, 135 insertions(+), 115 deletions(-) create mode 100644 src/renderer/src/components/UpdateDialog.tsx diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 1481d5acad..f58397d0f6 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -5,8 +5,8 @@ export enum IpcChannel { App_SetLanguage = 'app:set-language', App_SetEnableSpellCheck = 'app:set-enable-spell-check', App_SetSpellCheckLanguages = 'app:set-spell-check-languages', - App_ShowUpdateDialog = 'app:show-update-dialog', App_CheckForUpdate = 'app:check-for-update', + App_QuitAndInstall = 'app:quit-and-install', App_Reload = 'app:reload', App_Quit = 'app:quit', App_Info = 'app:info', @@ -229,7 +229,6 @@ export enum IpcChannel { // events BackupProgress = 'backup-progress', ThemeUpdated = 'theme:updated', - UpdateDownloadedCancelled = 'update-downloaded-cancelled', RestoreProgress = 'restore-progress', UpdateError = 'update-error', UpdateAvailable = 'update-available', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 275b3df4f9..a49d0535c5 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -132,7 +132,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url)) // Update - ipcMain.handle(IpcChannel.App_ShowUpdateDialog, () => appUpdater.showUpdateDialog(mainWindow)) + ipcMain.handle(IpcChannel.App_QuitAndInstall, () => appUpdater.quitAndInstall()) // language ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => { diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index 66b88bce84..ec0f1b97e0 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -1,17 +1,15 @@ import { loggerService } from '@logger' import { isWin } from '@main/constant' import { getIpCountry } from '@main/utils/ipService' -import { locales } from '@main/utils/locales' import { generateUserAgent } from '@main/utils/systemInfo' import { FeedUrl, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { CancellationToken, UpdateInfo } from 'builder-util-runtime' -import { app, BrowserWindow, dialog, net } from 'electron' +import { app, net } from 'electron' import { AppUpdater as _AppUpdater, autoUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater' import path from 'path' import semver from 'semver' -import icon from '../../../build/icon.png?asset' import { configManager } from './ConfigManager' import { windowService } from './WindowService' @@ -26,7 +24,6 @@ const LANG_MARKERS = { export default class AppUpdater { autoUpdater: _AppUpdater = autoUpdater - private releaseInfo: UpdateInfo | undefined private cancellationToken: CancellationToken = new CancellationToken() private updateCheckResult: UpdateCheckResult | null = null @@ -66,7 +63,6 @@ export default class AppUpdater { autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => { const processedReleaseInfo = this.processReleaseInfo(releaseInfo) windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo) - this.releaseInfo = processedReleaseInfo logger.info('update downloaded', processedReleaseInfo) }) @@ -247,37 +243,9 @@ export default class AppUpdater { } } - public async showUpdateDialog(mainWindow: BrowserWindow) { - if (!this.releaseInfo) { - return - } - const locale = locales[configManager.getLanguage()] - const { update: updateLocale } = locale.translation - - let detail = this.formatReleaseNotes(this.releaseInfo.releaseNotes) - if (detail === '') { - detail = updateLocale.noReleaseNotes - } - - dialog - .showMessageBox({ - type: 'info', - title: updateLocale.title, - icon, - message: updateLocale.message.replace('{{version}}', this.releaseInfo.version), - detail, - buttons: [updateLocale.later, updateLocale.install], - defaultId: 1, - cancelId: 0 - }) - .then(({ response }) => { - if (response === 1) { - app.isQuitting = true - setImmediate(() => autoUpdater.quitAndInstall()) - } else { - mainWindow.webContents.send(IpcChannel.UpdateDownloadedCancelled) - } - }) + public quitAndInstall() { + app.isQuitting = true + setImmediate(() => autoUpdater.quitAndInstall()) } /** @@ -349,38 +317,9 @@ export default class AppUpdater { return processedInfo } - - /** - * Format release notes for display - * @param releaseNotes - Release notes in various formats - * @returns Formatted string for display - */ - private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string { - if (!releaseNotes) { - return '' - } - - if (typeof releaseNotes === 'string') { - // Check if it contains multi-language markers - if (this.hasMultiLanguageMarkers(releaseNotes)) { - return this.parseMultiLangReleaseNotes(releaseNotes) - } - return releaseNotes - } - - if (Array.isArray(releaseNotes)) { - return releaseNotes.map((note) => note.note).join('\n') - } - - return '' - } } interface GithubReleaseInfo { draft: boolean prerelease: boolean tag_name: string } -interface ReleaseNoteInfo { - readonly version: string - readonly note: string | null -} diff --git a/src/main/services/__tests__/AppUpdater.test.ts b/src/main/services/__tests__/AppUpdater.test.ts index bb6a7827cb..4b3ac70d45 100644 --- a/src/main/services/__tests__/AppUpdater.test.ts +++ b/src/main/services/__tests__/AppUpdater.test.ts @@ -274,46 +274,4 @@ describe('AppUpdater', () => { expect(result.releaseNotes).toBeNull() }) }) - - describe('formatReleaseNotes', () => { - it('should format string release notes with markers', () => { - vi.mocked(configManager.getLanguage).mockReturnValue('en-US') - const notes = `English中文` - - const result = (appUpdater as any).formatReleaseNotes(notes) - - expect(result).toBe('English') - }) - - it('should format string release notes without markers', () => { - const notes = 'Simple notes' - - const result = (appUpdater as any).formatReleaseNotes(notes) - - expect(result).toBe('Simple notes') - }) - - it('should format array release notes', () => { - const notes = [ - { version: '1.0.0', note: 'Note 1' }, - { version: '1.0.1', note: 'Note 2' } - ] - - const result = (appUpdater as any).formatReleaseNotes(notes) - - expect(result).toBe('Note 1\nNote 2') - }) - - it('should handle null release notes', () => { - const result = (appUpdater as any).formatReleaseNotes(null) - - expect(result).toBe('') - }) - - it('should handle undefined release notes', () => { - const result = (appUpdater as any).formatReleaseNotes(undefined) - - expect(result).toBe('') - }) - }) }) diff --git a/src/preload/index.ts b/src/preload/index.ts index 9244076a9a..a2229c0ccb 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -51,7 +51,7 @@ const api = { setProxy: (proxy: string | undefined, bypassRules?: string) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules), checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate), - showUpdateDialog: () => ipcRenderer.invoke(IpcChannel.App_ShowUpdateDialog), + quitAndInstall: () => ipcRenderer.invoke(IpcChannel.App_QuitAndInstall), setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang), setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable), setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages), diff --git a/src/renderer/src/components/UpdateDialog.tsx b/src/renderer/src/components/UpdateDialog.tsx new file mode 100644 index 0000000000..e5cc8fa144 --- /dev/null +++ b/src/renderer/src/components/UpdateDialog.tsx @@ -0,0 +1,101 @@ +import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ScrollShadow } from '@heroui/react' +import { loggerService } from '@logger' +import { handleSaveData } from '@renderer/store' +import { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import Markdown from 'react-markdown' + +const logger = loggerService.withContext('UpdateDialog') + +interface UpdateDialogProps { + isOpen: boolean + onClose: () => void + releaseInfo: UpdateInfo | null +} + +const UpdateDialog: React.FC = ({ isOpen, onClose, releaseInfo }) => { + const { t } = useTranslation() + const [isInstalling, setIsInstalling] = useState(false) + + useEffect(() => { + if (isOpen && releaseInfo) { + logger.info('Update dialog opened', { version: releaseInfo.version }) + } + }, [isOpen, releaseInfo]) + + const handleInstall = async () => { + setIsInstalling(true) + try { + await handleSaveData() + await window.api.quitAndInstall() + } catch (error) { + logger.error('Failed to save data before update', error as Error) + setIsInstalling(false) + window.toast.error(t('update.saveDataError')) + } + } + + const releaseNotes = releaseInfo?.releaseNotes + + return ( + + + {(onModalClose) => ( + <> + +

{t('update.title')}

+

+ {t('update.message').replace('{{version}}', releaseInfo?.version || '')} +

+
+ + + +
+ + {typeof releaseNotes === 'string' + ? releaseNotes + : Array.isArray(releaseNotes) + ? releaseNotes + .map((note: ReleaseNoteInfo) => note.note) + .filter(Boolean) + .join('\n\n') + : t('update.noReleaseNotes')} + +
+
+
+ + + + + + + + )} +
+
+ ) +} + +export default UpdateDialog diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 00ab42128b..aa7121348c 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -4412,6 +4412,7 @@ "later": "Later", "message": "New version {{version}} is ready, do you want to install it now?", "noReleaseNotes": "No release notes", + "saveDataError": "Failed to save data, please try again.", "title": "Update" }, "warning": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index af2389dbe2..0d7f3df613 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -4412,6 +4412,7 @@ "later": "稍后", "message": "发现新版本 {{version}},是否立即安装?", "noReleaseNotes": "暂无更新日志", + "saveDataError": "保存数据失败,请重试", "title": "更新提示" }, "warning": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index d0c08124ea..56eb1c1bb7 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -4412,6 +4412,7 @@ "later": "稍後", "message": "新版本 {{version}} 已準備就緒,是否立即安裝?", "noReleaseNotes": "暫無更新日誌", + "saveDataError": "保存數據失敗,請重試", "title": "更新提示" }, "warning": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 57edf11f96..a138242daf 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -4412,6 +4412,7 @@ "later": "Μετά", "message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;", "noReleaseNotes": "Χωρίς σημειώσεις", + "saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά", "title": "Ενημέρωση" }, "warning": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 739943ffc7..8b9effc54a 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -4412,6 +4412,7 @@ "later": "Más tarde", "message": "Nueva versión {{version}} disponible, ¿desea instalarla ahora?", "noReleaseNotes": "Sin notas de la versión", + "saveDataError": "Error al guardar los datos, inténtalo de nuevo", "title": "Actualización" }, "warning": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index d5eda2e61e..dfac0946b7 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -4412,6 +4412,7 @@ "later": "Plus tard", "message": "Nouvelle version {{version}} disponible, voulez-vous l'installer maintenant ?", "noReleaseNotes": "Aucune note de version", + "saveDataError": "Échec de la sauvegarde des données, veuillez réessayer", "title": "Mise à jour" }, "warning": { diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index f5cde82e28..b56cef14a1 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -4412,6 +4412,7 @@ "later": "後で", "message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?", "noReleaseNotes": "暫無更新日誌", + "saveDataError": "データの保存に失敗しました。もう一度お試しください。", "title": "更新" }, "warning": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index befcedf381..0af090bfe6 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -4412,6 +4412,7 @@ "later": "Mais tarde", "message": "Nova versão {{version}} disponível, deseja instalar agora?", "noReleaseNotes": "Sem notas de versão", + "saveDataError": "Falha ao salvar os dados, tente novamente", "title": "Atualização" }, "warning": { diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index f74529300d..6772446361 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -4412,6 +4412,7 @@ "later": "Позже", "message": "Новая версия {{version}} готова, установить сейчас?", "noReleaseNotes": "Нет заметок об обновлении", + "saveDataError": "Ошибка сохранения данных, повторите попытку", "title": "Обновление" }, "warning": { diff --git a/src/renderer/src/pages/home/components/UpdateAppButton.tsx b/src/renderer/src/pages/home/components/UpdateAppButton.tsx index 997590ef10..4d8f3f1261 100644 --- a/src/renderer/src/pages/home/components/UpdateAppButton.tsx +++ b/src/renderer/src/pages/home/components/UpdateAppButton.tsx @@ -1,4 +1,6 @@ import { SyncOutlined } from '@ant-design/icons' +import { useDisclosure } from '@heroui/react' +import UpdateDialog from '@renderer/components/UpdateDialog' import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { Button } from 'antd' @@ -10,6 +12,7 @@ const UpdateAppButton: FC = () => { const { update } = useRuntime() const { autoCheckUpdate } = useSettings() const { t } = useTranslation() + const { isOpen, onOpen, onClose } = useDisclosure() if (!update) { return null @@ -23,13 +26,15 @@ const UpdateAppButton: FC = () => { window.api.showUpdateDialog()} + onClick={onOpen} icon={} color="orange" variant="outlined" size="small"> {t('button.update_available')} + + ) } diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index d611ed458e..1cbb8ce694 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -1,18 +1,21 @@ import { GithubOutlined } from '@ant-design/icons' +import { useDisclosure } from '@heroui/react' import IndicatorLight from '@renderer/components/IndicatorLight' import { HStack } from '@renderer/components/Layout' +import UpdateDialog from '@renderer/components/UpdateDialog' import { APP_NAME, AppLogo } from '@renderer/config/env' import { useTheme } from '@renderer/context/ThemeProvider' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import i18n from '@renderer/i18n' -import { handleSaveData, useAppDispatch } from '@renderer/store' +import { useAppDispatch } from '@renderer/store' import { setUpdateState } from '@renderer/store/runtime' import { ThemeMode } from '@renderer/types' import { runAsyncFunction } from '@renderer/utils' import { UpgradeChannel } from '@shared/config/constant' import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd' +import { UpdateInfo } from 'builder-util-runtime' import { debounce } from 'lodash' import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react' import { BadgeQuestionMark } from 'lucide-react' @@ -27,6 +30,8 @@ import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitl const AboutSettings: FC = () => { const [version, setVersion] = useState('') const [isPortable, setIsPortable] = useState(false) + const [updateDialogInfo, setUpdateDialogInfo] = useState(null) + const { isOpen, onOpen, onClose } = useDisclosure() const { t } = useTranslation() const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings() const { theme } = useTheme() @@ -41,8 +46,9 @@ const AboutSettings: FC = () => { } if (update.downloaded) { - await handleSaveData() - window.api.showUpdateDialog() + // Open update dialog directly in renderer + setUpdateDialogInfo(update.info || null) + onOpen() return } @@ -341,6 +347,9 @@ const AboutSettings: FC = () => { + + {/* Update Dialog */} + ) }