feat: add update info ui
This commit is contained in:
parent
aeff59946c
commit
b5a109401c
@ -1,6 +1,8 @@
|
||||
# provider: generic
|
||||
# url: http://127.0.0.1:8080
|
||||
# updaterCacheDirName: cherry-studio-updater
|
||||
provider: github
|
||||
repo: cherry-studio
|
||||
owner: kangfenmao
|
||||
# provider: github
|
||||
# repo: cherry-studio
|
||||
# owner: kangfenmao
|
||||
provider: generic
|
||||
url: https://cherrystudio.ocool.online
|
||||
|
||||
@ -55,9 +55,8 @@ appImage:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
npmRebuild: false
|
||||
publish:
|
||||
provider: github
|
||||
repo: cherry-studio
|
||||
owner: kangfenmao
|
||||
provider: generic
|
||||
url: https://cherrystudio.ocool.online
|
||||
electronDownload:
|
||||
mirror: https://npmmirror.com/mirrors/electron/
|
||||
afterSign: scripts/notarize.js
|
||||
|
||||
@ -25,7 +25,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: []
|
||||
exclude: ['chunk-7UIZINC5.js', 'chunk-7OJJKI46.js']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -2,6 +2,8 @@ import { app, BrowserWindow, dialog } from 'electron'
|
||||
import logger from 'electron-log'
|
||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
||||
|
||||
import icon from '../../../build/icon.png?asset'
|
||||
|
||||
export default class AppUpdater {
|
||||
autoUpdater: _AppUpdater = autoUpdater
|
||||
|
||||
@ -43,6 +45,7 @@ export default class AppUpdater {
|
||||
.showMessageBox({
|
||||
type: 'info',
|
||||
title: '安装更新',
|
||||
icon,
|
||||
message: `新版本 ${releaseInfo.version} 已准备就绪`,
|
||||
detail: this.formatReleaseNotes(releaseInfo.releaseNotes),
|
||||
buttons: ['稍后安装', '立即安装'],
|
||||
|
||||
3
src/preload/index.d.ts
vendored
3
src/preload/index.d.ts
vendored
@ -3,6 +3,7 @@ import { FileType } from '@renderer/types'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
import { AppInfo, LanguageVarious } from '@renderer/types'
|
||||
import type { OpenDialogOptions } from 'electron'
|
||||
import type { UpdateInfo } from 'electron-updater'
|
||||
import { Readable } from 'stream'
|
||||
|
||||
declare global {
|
||||
@ -10,7 +11,7 @@ declare global {
|
||||
electron: ElectronAPI
|
||||
api: {
|
||||
getAppInfo: () => Promise<AppInfo>
|
||||
checkForUpdate: () => void
|
||||
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
setLanguage: (theme: LanguageVarious) => void
|
||||
|
||||
35
src/renderer/src/components/IndicatorLight.tsx
Normal file
35
src/renderer/src/components/IndicatorLight.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
// src/renderer/src/components/IndicatorLight.tsx
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface IndicatorLightProps {
|
||||
color: string
|
||||
}
|
||||
|
||||
const Light = styled.div<{ color: string }>`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: ${({ color }) => color};
|
||||
box-shadow: 0 0 6px ${({ color }) => color};
|
||||
animation: pulse 2s infinite;
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const IndicatorLight: React.FC<IndicatorLightProps> = ({ color }) => {
|
||||
const actualColor = color === 'green' ? '#22c55e' : color
|
||||
return <Light color={actualColor} />
|
||||
}
|
||||
|
||||
export default IndicatorLight
|
||||
@ -3,14 +3,15 @@ import { isLocalAi } from '@renderer/config/env'
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar, setFilesPath } from '@renderer/store/runtime'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import { useRuntime } from './useRuntime'
|
||||
import { useSettings } from './useSettings'
|
||||
import useUpdateHandler from './useUpdateHandler'
|
||||
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
@ -19,6 +20,8 @@ export function useAppInit() {
|
||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
|
||||
useUpdateHandler()
|
||||
|
||||
useEffect(() => {
|
||||
avatar?.value && dispatch(setAvatar(avatar.value))
|
||||
}, [avatar, dispatch])
|
||||
@ -28,11 +31,12 @@ export function useAppInit() {
|
||||
runAsyncFunction(async () => {
|
||||
const { isPackaged } = await window.api.getAppInfo()
|
||||
if (isPackaged && !manualUpdateCheck) {
|
||||
setTimeout(window.api.checkForUpdate, 3000)
|
||||
await delay(2)
|
||||
const { updateInfo } = await window.api.checkForUpdate()
|
||||
dispatch(setUpdateState({ info: updateInfo }))
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, [dispatch, manualUpdateCheck])
|
||||
|
||||
useEffect(() => {
|
||||
if (proxyMode === 'system') {
|
||||
|
||||
64
src/renderer/src/hooks/useUpdateHandler.ts
Normal file
64
src/renderer/src/hooks/useUpdateHandler.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setUpdateState } from '@renderer/store/runtime'
|
||||
import type { ProgressInfo, UpdateInfo } from 'electron-updater'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function useUpdateHandler() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const ipcRenderer = window.electron.ipcRenderer
|
||||
const removers = [
|
||||
ipcRenderer.on('update-not-available', () => {
|
||||
dispatch(setUpdateState({ checking: false }))
|
||||
window.message.success(t('settings.about.updateNotAvailable'))
|
||||
}),
|
||||
ipcRenderer.on('update-available', (_, releaseInfo: UpdateInfo) => {
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
checking: false,
|
||||
downloading: true,
|
||||
info: releaseInfo,
|
||||
available: true
|
||||
})
|
||||
)
|
||||
}),
|
||||
ipcRenderer.on('download-update', () => {
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
checking: false,
|
||||
downloading: true
|
||||
})
|
||||
)
|
||||
}),
|
||||
ipcRenderer.on('download-progress', (_, progress: ProgressInfo) => {
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
downloading: progress.percent < 100,
|
||||
downloadProgress: progress.percent
|
||||
})
|
||||
)
|
||||
}),
|
||||
ipcRenderer.on('update-downloaded', () => {
|
||||
dispatch(setUpdateState({ downloading: false }))
|
||||
}),
|
||||
ipcRenderer.on('update-error', (_, error) => {
|
||||
dispatch(
|
||||
setUpdateState({
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloadProgress: 0
|
||||
})
|
||||
)
|
||||
window.modal.info({
|
||||
title: t('settings.about.updateError'),
|
||||
content: error?.message || t('settings.about.updateError'),
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
]
|
||||
return () => removers.forEach((remover) => remover())
|
||||
}, [dispatch, t])
|
||||
}
|
||||
@ -316,6 +316,7 @@
|
||||
"settings": {
|
||||
"about": "About & Feedback",
|
||||
"about.checkUpdate": "Check Update",
|
||||
"about.checkUpdate.available": "Update",
|
||||
"about.checkingUpdate": "Checking for updates...",
|
||||
"about.contact.button": "Email",
|
||||
"about.contact.title": "Contact",
|
||||
|
||||
@ -316,6 +316,7 @@
|
||||
"settings": {
|
||||
"about": "О программе и обратная связь",
|
||||
"about.checkUpdate": "Проверить обновления",
|
||||
"about.checkUpdate.available": "Обновить",
|
||||
"about.checkingUpdate": "Проверка обновлений...",
|
||||
"about.contact.button": "Электронная почта",
|
||||
"about.contact.title": "Контакты",
|
||||
|
||||
@ -316,6 +316,7 @@
|
||||
"settings": {
|
||||
"about": "关于我们",
|
||||
"about.checkUpdate": "检查更新",
|
||||
"about.checkUpdate.available": "立即更新",
|
||||
"about.checkingUpdate": "正在检查更新...",
|
||||
"about.contact.button": "邮件",
|
||||
"about.contact.title": "邮件联系",
|
||||
|
||||
@ -316,6 +316,7 @@
|
||||
"settings": {
|
||||
"about": "關於與回饋",
|
||||
"about.checkUpdate": "檢查更新",
|
||||
"about.checkUpdate.available": "立即更新",
|
||||
"about.checkingUpdate": "正在檢查更新...",
|
||||
"about.contact.button": "郵件",
|
||||
"about.contact.title": "聯繫方式",
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import { GithubOutlined } from '@ant-design/icons'
|
||||
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from '@ant-design/icons'
|
||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { APP_NAME, AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setUpdateState } from '@renderer/store/runtime'
|
||||
import { setManualUpdateCheck } from '@renderer/store/settings'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { Avatar, Button, Progress, Row, Switch, Tag } from 'antd'
|
||||
import { ProgressInfo, UpdateInfo } from 'electron-updater'
|
||||
import { debounce } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -22,17 +24,18 @@ import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitl
|
||||
const AboutSettings: FC = () => {
|
||||
const [version, setVersion] = useState('')
|
||||
const { t } = useTranslation()
|
||||
const [percent, setPercent] = useState(0)
|
||||
const [checkUpdateLoading, setCheckUpdateLoading] = useState(false)
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const { manualUpdateCheck } = useSettings()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { update } = useRuntime()
|
||||
|
||||
const onCheckUpdate = debounce(
|
||||
async () => {
|
||||
if (checkUpdateLoading || downloading) return
|
||||
setCheckUpdateLoading(true)
|
||||
if (update.checking || update.downloading) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(setUpdateState({ checking: true }))
|
||||
|
||||
try {
|
||||
await window.api.checkForUpdate()
|
||||
@ -40,7 +43,7 @@ const AboutSettings: FC = () => {
|
||||
window.message.error(t('settings.about.updateError'))
|
||||
}
|
||||
|
||||
setCheckUpdateLoading(false)
|
||||
dispatch(setUpdateState({ checking: false }))
|
||||
},
|
||||
2000,
|
||||
{ leading: true, trailing: false }
|
||||
@ -75,52 +78,6 @@ const AboutSettings: FC = () => {
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const ipcRenderer = window.electron.ipcRenderer
|
||||
const removers = [
|
||||
ipcRenderer.on('update-not-available', () => {
|
||||
setCheckUpdateLoading(false)
|
||||
window.message.success(t('settings.about.updateNotAvailable'))
|
||||
}),
|
||||
ipcRenderer.on('update-available', (_, releaseInfo: UpdateInfo) => {
|
||||
setCheckUpdateLoading(false)
|
||||
setDownloading(true)
|
||||
window.modal.info({
|
||||
title: t('settings.about.updateAvailable', { version: releaseInfo.version }),
|
||||
content: (
|
||||
<Markdown>
|
||||
{typeof releaseInfo.releaseNotes === 'string'
|
||||
? releaseInfo.releaseNotes
|
||||
: releaseInfo.releaseNotes?.map((note) => note.note).join('\n')}
|
||||
</Markdown>
|
||||
)
|
||||
})
|
||||
}),
|
||||
ipcRenderer.on('download-update', () => {
|
||||
setCheckUpdateLoading(false)
|
||||
setDownloading(true)
|
||||
}),
|
||||
ipcRenderer.on('download-progress', (_, progress: ProgressInfo) => {
|
||||
setPercent(progress.percent)
|
||||
setDownloading(progress.percent < 100)
|
||||
}),
|
||||
ipcRenderer.on('update-downloaded', () => {
|
||||
setDownloading(false)
|
||||
}),
|
||||
ipcRenderer.on('update-error', (_, error) => {
|
||||
setCheckUpdateLoading(false)
|
||||
setDownloading(false)
|
||||
setPercent(0)
|
||||
window.modal.info({
|
||||
title: t('settings.about.updateError'),
|
||||
content: error?.message || t('settings.about.updateError'),
|
||||
icon: null
|
||||
})
|
||||
})
|
||||
]
|
||||
return () => removers.forEach((remover) => remover())
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<SettingGroup theme={theme}>
|
||||
@ -136,11 +93,11 @@ const AboutSettings: FC = () => {
|
||||
<AboutHeader>
|
||||
<Row align="middle">
|
||||
<AvatarWrapper onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio')}>
|
||||
{percent > 0 && (
|
||||
{update.downloadProgress > 0 && (
|
||||
<ProgressCircle
|
||||
type="circle"
|
||||
size={84}
|
||||
percent={percent}
|
||||
percent={update.downloadProgress}
|
||||
showInfo={false}
|
||||
strokeLinecap="butt"
|
||||
strokeColor="#67ad5b"
|
||||
@ -161,9 +118,13 @@ const AboutSettings: FC = () => {
|
||||
</Row>
|
||||
<CheckUpdateButton
|
||||
onClick={onCheckUpdate}
|
||||
loading={checkUpdateLoading}
|
||||
disabled={downloading || checkUpdateLoading}>
|
||||
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
|
||||
loading={update.checking}
|
||||
disabled={update.downloading || update.checking}>
|
||||
{update.downloading
|
||||
? t('settings.about.downloading')
|
||||
: update.available
|
||||
? t('settings.about.checkUpdate.available')
|
||||
: t('settings.about.checkUpdate')}
|
||||
</CheckUpdateButton>
|
||||
</AboutHeader>
|
||||
<SettingDivider />
|
||||
@ -172,6 +133,23 @@ const AboutSettings: FC = () => {
|
||||
<Switch value={manualUpdateCheck} onChange={(v) => dispatch(setManualUpdateCheck(v))} />
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
{update.info && (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.about.updateAvailable', { version: update.info.version })}
|
||||
<IndicatorLight color="green" />
|
||||
</SettingRowTitle>
|
||||
</SettingRow>
|
||||
<UpdateNotesWrapper>
|
||||
<Markdown>
|
||||
{typeof update.info.releaseNotes === 'string'
|
||||
? update.info.releaseNotes.replaceAll('\n', '\n\n')
|
||||
: update.info.releaseNotes?.map((note) => note.note).join('\n')}
|
||||
</Markdown>
|
||||
</UpdateNotesWrapper>
|
||||
</SettingGroup>
|
||||
)}
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
@ -285,4 +263,17 @@ export const SettingRowTitle = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const UpdateNotesWrapper = styled.div`
|
||||
padding: 12px 0;
|
||||
margin: 8px 0;
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 6px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
export default AboutSettings
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||
import type { UpdateInfo } from 'electron-updater'
|
||||
|
||||
export interface UpdateState {
|
||||
info: UpdateInfo | null
|
||||
checking: boolean
|
||||
downloading: boolean
|
||||
downloadProgress: number
|
||||
available: boolean
|
||||
}
|
||||
|
||||
export interface RuntimeState {
|
||||
avatar: string
|
||||
@ -7,6 +16,7 @@ export interface RuntimeState {
|
||||
minappShow: boolean
|
||||
searching: boolean
|
||||
filesPath: string
|
||||
update: UpdateState
|
||||
}
|
||||
|
||||
const initialState: RuntimeState = {
|
||||
@ -14,7 +24,14 @@ const initialState: RuntimeState = {
|
||||
generating: false,
|
||||
minappShow: false,
|
||||
searching: false,
|
||||
filesPath: ''
|
||||
filesPath: '',
|
||||
update: {
|
||||
info: null,
|
||||
checking: false,
|
||||
downloading: false,
|
||||
downloadProgress: 0,
|
||||
available: false
|
||||
}
|
||||
}
|
||||
|
||||
const runtimeSlice = createSlice({
|
||||
@ -35,10 +52,14 @@ const runtimeSlice = createSlice({
|
||||
},
|
||||
setFilesPath: (state, action: PayloadAction<string>) => {
|
||||
state.filesPath = action.payload
|
||||
},
|
||||
setUpdateState: (state, action: PayloadAction<Partial<UpdateState>>) => {
|
||||
state.update = { ...state.update, ...action.payload }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { setAvatar, setGenerating, setMinappShow, setSearching, setFilesPath } = runtimeSlice.actions
|
||||
export const { setAvatar, setGenerating, setMinappShow, setSearching, setFilesPath, setUpdateState } =
|
||||
runtimeSlice.actions
|
||||
|
||||
export default runtimeSlice.reducer
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user