feat(app-update): Refactor update handling and add manual update dialog

- Modify AppUpdater to separate update dialog logic
- Add new IPC handler for manually showing update dialog
- Update renderer hooks and store to track downloaded update state
- Switch import for UpdateInfo from electron-updater to builder-util-runtime
This commit is contained in:
kangfenmao 2025-03-05 14:33:10 +08:00
parent b2b89a1339
commit dcaac54c75
14 changed files with 114 additions and 36 deletions

View File

@ -27,7 +27,7 @@ const backupManager = new BackupManager()
const exportService = new ExportService(fileManager) const exportService = new ExportService(fileManager)
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow) const appUpdater = new AppUpdater(mainWindow)
ipcMain.handle('app:info', () => ({ ipcMain.handle('app:info', () => ({
version: app.getVersion(), version: app.getVersion(),
@ -48,6 +48,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('app:reload', () => mainWindow.reload()) ipcMain.handle('app:reload', () => mainWindow.reload())
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url)) ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
// Update
ipcMain.handle('app:show-update-dialog', () => appUpdater.showUpdateDialog(mainWindow))
// language // language
ipcMain.handle('app:set-language', (_, language) => { ipcMain.handle('app:set-language', (_, language) => {
configManager.setLanguage(language) configManager.setLanguage(language)
@ -99,9 +102,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
// check for update // check for update
ipcMain.handle('app:check-for-update', async () => { ipcMain.handle('app:check-for-update', async () => {
const update = await autoUpdater.checkForUpdates() const update = await appUpdater.autoUpdater.checkForUpdates()
return { return {
currentVersion: autoUpdater.currentVersion, currentVersion: appUpdater.autoUpdater.currentVersion,
updateInfo: update?.updateInfo updateInfo: update?.updateInfo
} }
}) })

View File

@ -1,11 +1,13 @@
import { UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog } from 'electron' import { app, BrowserWindow, dialog } from 'electron'
import logger from 'electron-log' import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater' import { AppUpdater as _AppUpdater, autoUpdater } from 'electron-updater'
import icon from '../../../build/icon.png?asset' import icon from '../../../build/icon.png?asset'
export default class AppUpdater { export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater autoUpdater: _AppUpdater = autoUpdater
private releaseInfo: UpdateInfo | undefined
constructor(mainWindow: BrowserWindow) { constructor(mainWindow: BrowserWindow) {
logger.transports.file.level = 'info' logger.transports.file.level = 'info'
@ -37,34 +39,40 @@ export default class AppUpdater {
// 当需要更新的内容下载完成后 // 当需要更新的内容下载完成后
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => { autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
mainWindow.webContents.send('update-downloaded') mainWindow.webContents.send('update-downloaded', releaseInfo)
this.releaseInfo = releaseInfo
logger.info('下载完成,询问用户是否更新', releaseInfo) logger.info('下载完成', releaseInfo)
dialog
.showMessageBox({
type: 'info',
title: '安装更新',
icon,
message: `新版本 ${releaseInfo.version} 已准备就绪`,
detail: this.formatReleaseNotes(releaseInfo.releaseNotes),
buttons: ['稍后安装', '立即安装'],
defaultId: 1,
cancelId: 0
})
.then(({ response }) => {
if (response === 1) {
app.isQuitting = true
setImmediate(() => autoUpdater.quitAndInstall())
} else {
mainWindow.webContents.send('update-downloaded-cancelled')
}
})
}) })
this.autoUpdater = autoUpdater this.autoUpdater = autoUpdater
} }
public async showUpdateDialog(mainWindow: BrowserWindow) {
if (!this.releaseInfo) {
return
}
dialog
.showMessageBox({
type: 'info',
title: '安装更新',
icon,
message: `新版本 ${this.releaseInfo.version} 已准备就绪`,
detail: this.formatReleaseNotes(this.releaseInfo.releaseNotes),
buttons: ['稍后安装', '立即安装'],
defaultId: 1,
cancelId: 0
})
.then(({ response }) => {
if (response === 1) {
app.isQuitting = true
setImmediate(() => autoUpdater.quitAndInstall())
} else {
mainWindow.webContents.send('update-downloaded-cancelled')
}
})
}
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string { private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
if (!releaseNotes) { if (!releaseNotes) {
return '暂无更新说明' return '暂无更新说明'

View File

@ -15,6 +15,7 @@ declare global {
api: { api: {
getAppInfo: () => Promise<AppInfo> getAppInfo: () => Promise<AppInfo>
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }> checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
showUpdateDialog: () => Promise<void>
openWebsite: (url: string) => void openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void setProxy: (proxy: string | undefined) => void
setLanguage: (theme: LanguageVarious) => void setLanguage: (theme: LanguageVarious) => void

View File

@ -8,6 +8,7 @@ const api = {
reload: () => ipcRenderer.invoke('app:reload'), reload: () => ipcRenderer.invoke('app:reload'),
setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy), setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy),
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'), checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
showUpdateDialog: () => ipcRenderer.invoke('app:show-update-dialog'),
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang), setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive), setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
restartTray: () => ipcRenderer.invoke('app:restart-tray'), restartTray: () => ipcRenderer.invoke('app:restart-tray'),

View File

@ -1,6 +1,6 @@
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setUpdateState } from '@renderer/store/runtime' import { setUpdateState } from '@renderer/store/runtime'
import type { ProgressInfo, UpdateInfo } from 'electron-updater' import type { ProgressInfo, UpdateInfo } from 'builder-util-runtime'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -46,8 +46,14 @@ export default function useUpdateHandler() {
}) })
) )
}), }),
ipcRenderer.on('update-downloaded', () => { ipcRenderer.on('update-downloaded', (_, releaseInfo: UpdateInfo) => {
dispatch(setUpdateState({ downloading: false })) dispatch(
setUpdateState({
downloading: false,
info: releaseInfo,
downloaded: true
})
)
}), }),
ipcRenderer.on('update-error', (_, error) => { ipcRenderer.on('update-error', (_, error) => {
dispatch( dispatch(

View File

@ -68,7 +68,8 @@
"collapse": "Collapse", "collapse": "Collapse",
"manage": "Manage", "manage": "Manage",
"select_model": "Select Model", "select_model": "Select Model",
"show.all": "Show All" "show.all": "Show All",
"update_available": "Update Available"
}, },
"chat": { "chat": {
"add.assistant.title": "Add Assistant", "add.assistant.title": "Add Assistant",

View File

@ -68,7 +68,8 @@
"collapse": "折りたたむ", "collapse": "折りたたむ",
"manage": "管理", "manage": "管理",
"select_model": "モデルを選択", "select_model": "モデルを選択",
"show.all": "すべて表示" "show.all": "すべて表示",
"update_available": "更新可能"
}, },
"chat": { "chat": {
"add.assistant.title": "アシスタントを追加", "add.assistant.title": "アシスタントを追加",

View File

@ -68,7 +68,8 @@
"collapse": "Свернуть", "collapse": "Свернуть",
"manage": "Редактировать", "manage": "Редактировать",
"select_model": "Выбрать модель", "select_model": "Выбрать модель",
"show.all": "Показать все" "show.all": "Показать все",
"update_available": "Доступно обновление"
}, },
"chat": { "chat": {
"add.assistant.title": "Добавить ассистента", "add.assistant.title": "Добавить ассистента",

View File

@ -68,7 +68,8 @@
"collapse": "收起", "collapse": "收起",
"manage": "管理", "manage": "管理",
"select_model": "选择模型", "select_model": "选择模型",
"show.all": "显示全部" "show.all": "显示全部",
"update_available": "有可用更新"
}, },
"chat": { "chat": {
"add.assistant.title": "添加助手", "add.assistant.title": "添加助手",

View File

@ -68,7 +68,8 @@
"collapse": "收起", "collapse": "收起",
"manage": "管理", "manage": "管理",
"select_model": "選擇模型", "select_model": "選擇模型",
"show.all": "顯示全部" "show.all": "顯示全部",
"update_available": "有可用更新"
}, },
"chat": { "chat": {
"add.assistant.title": "添加助手", "add.assistant.title": "添加助手",

View File

@ -18,6 +18,7 @@ import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton' import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
interface Props { interface Props {
activeAssistant: Assistant activeAssistant: Assistant
@ -83,6 +84,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
<SelectModelButton assistant={assistant} /> <SelectModelButton assistant={assistant} />
</HStack> </HStack>
<HStack alignItems="center" gap={8}> <HStack alignItems="center" gap={8}>
<UpdateAppButton />
<NarrowIcon onClick={() => SearchPopup.show()}> <NarrowIcon onClick={() => SearchPopup.show()}>
<SearchOutlined /> <SearchOutlined />
</NarrowIcon> </NarrowIcon>

View File

@ -0,0 +1,45 @@
import { SyncOutlined } from '@ant-design/icons'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { Button } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const UpdateAppButton: FC = () => {
const { update } = useRuntime()
const { t } = useTranslation()
if (!update) {
return null
}
if (!update.downloaded) {
return null
}
return (
<Container>
<UpdateButton
className="nodrag"
onClick={() => window.api.showUpdateDialog()}
icon={<SyncOutlined />}
color="orange"
variant="outlined"
size="small">
{t('button.update_available')}
</UpdateButton>
</Container>
)
}
const Container = styled.div``
const UpdateButton = styled(Button)`
border-radius: 24px;
font-size: 12px;
@media (max-width: 1000px) {
display: none;
}
`
export default UpdateAppButton

View File

@ -36,6 +36,11 @@ const AboutSettings: FC = () => {
return return
} }
if (update.downloaded) {
window.api.showUpdateDialog()
return
}
dispatch(setUpdateState({ checking: true })) dispatch(setUpdateState({ checking: true }))
try { try {

View File

@ -1,11 +1,12 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { AppLogo, UserAvatar } from '@renderer/config/env' import { AppLogo, UserAvatar } from '@renderer/config/env'
import type { UpdateInfo } from 'electron-updater' import type { UpdateInfo } from 'builder-util-runtime'
export interface UpdateState { export interface UpdateState {
info: UpdateInfo | null info: UpdateInfo | null
checking: boolean checking: boolean
downloading: boolean downloading: boolean
downloaded: boolean
downloadProgress: number downloadProgress: number
available: boolean available: boolean
} }
@ -43,6 +44,7 @@ const initialState: RuntimeState = {
info: null, info: null,
checking: false, checking: false,
downloading: false, downloading: false,
downloaded: false,
downloadProgress: 0, downloadProgress: 0,
available: false available: false
}, },