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:
parent
b2b89a1339
commit
dcaac54c75
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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 '暂无更新说明'
|
||||||
|
|||||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@ -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
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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": "アシスタントを追加",
|
||||||
|
|||||||
@ -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": "Добавить ассистента",
|
||||||
|
|||||||
@ -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": "添加助手",
|
||||||
|
|||||||
@ -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": "添加助手",
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
45
src/renderer/src/pages/home/components/UpdateAppButton.tsx
Normal file
45
src/renderer/src/pages/home/components/UpdateAppButton.tsx
Normal 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
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user