mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 07:00:09 +08:00
refactor: replace UpdateDialog with UpdateDialogPopup for better modal handling
This commit is contained in:
parent
468aebd632
commit
b334a2c5be
205
src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Normal file
205
src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { TopView } from '@renderer/components/TopView'
|
||||||
|
import { handleSaveData } from '@renderer/store'
|
||||||
|
import { Button, Modal } from 'antd'
|
||||||
|
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import Markdown from 'react-markdown'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('UpdateDialog')
|
||||||
|
|
||||||
|
interface ShowParams {
|
||||||
|
releaseInfo: UpdateInfo | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends ShowParams {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ releaseInfo, resolve }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const [isInstalling, setIsInstalling] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (releaseInfo) {
|
||||||
|
logger.info('Update dialog opened', { version: releaseInfo.version })
|
||||||
|
}
|
||||||
|
}, [releaseInfo])
|
||||||
|
|
||||||
|
const handleInstall = async () => {
|
||||||
|
setIsInstalling(true)
|
||||||
|
try {
|
||||||
|
await handleSaveData()
|
||||||
|
await window.api.quitAndInstall()
|
||||||
|
setOpen(false)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to save data before update', error as Error)
|
||||||
|
setIsInstalling(false)
|
||||||
|
window.toast.error(t('update.saveDataError'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resolve({})
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateDialogPopup.hide = onCancel
|
||||||
|
|
||||||
|
const releaseNotes = releaseInfo?.releaseNotes
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<ModalHeaderWrapper>
|
||||||
|
<h3>{t('update.title')}</h3>
|
||||||
|
<p>{t('update.message').replace('{{version}}', releaseInfo?.version || '')}</p>
|
||||||
|
</ModalHeaderWrapper>
|
||||||
|
}
|
||||||
|
open={open}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
transitionName="animation-move-down"
|
||||||
|
centered
|
||||||
|
width={720}
|
||||||
|
footer={[
|
||||||
|
<Button key="later" onClick={onCancel} disabled={isInstalling}>
|
||||||
|
{t('update.later')}
|
||||||
|
</Button>,
|
||||||
|
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
|
||||||
|
{t('update.install')}
|
||||||
|
</Button>
|
||||||
|
]}>
|
||||||
|
<ModalBodyWrapper>
|
||||||
|
<ReleaseNotesWrapper className="markdown">
|
||||||
|
<Markdown>
|
||||||
|
{typeof releaseNotes === 'string'
|
||||||
|
? releaseNotes
|
||||||
|
: Array.isArray(releaseNotes)
|
||||||
|
? releaseNotes
|
||||||
|
.map((note: ReleaseNoteInfo) => note.note)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n\n')
|
||||||
|
: t('update.noReleaseNotes')}
|
||||||
|
</Markdown>
|
||||||
|
</ReleaseNotesWrapper>
|
||||||
|
</ModalBodyWrapper>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopViewKey = 'UpdateDialogPopup'
|
||||||
|
|
||||||
|
export default class UpdateDialogPopup {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}
|
||||||
|
static show(props: ShowParams) {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PopupContainer
|
||||||
|
{...props}
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
TopViewKey
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalHeaderWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ModalBodyWrapper = styled.div`
|
||||||
|
max-height: 450px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ReleaseNotesWrapper = styled.div`
|
||||||
|
background-color: var(--color-bg-2);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 16px 0 8px 0;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 8px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 2px 6px;
|
||||||
|
background-color: var(--color-bg-3);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 12px;
|
||||||
|
background-color: var(--color-bg-3);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -1,101 +0,0 @@
|
|||||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ScrollShadow } from '@heroui/react'
|
|
||||||
import { loggerService } from '@logger'
|
|
||||||
import { handleSaveData } from '@renderer/store'
|
|
||||||
import type { 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<UpdateDialogProps> = ({ 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 (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
size="2xl"
|
|
||||||
scrollBehavior="inside"
|
|
||||||
classNames={{
|
|
||||||
base: 'max-h-[85vh]',
|
|
||||||
header: 'border-b border-divider',
|
|
||||||
footer: 'border-t border-divider'
|
|
||||||
}}>
|
|
||||||
<ModalContent>
|
|
||||||
{(onModalClose) => (
|
|
||||||
<>
|
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
|
||||||
<h3 className="font-semibold text-lg">{t('update.title')}</h3>
|
|
||||||
<p className="text-default-500 text-small">
|
|
||||||
{t('update.message').replace('{{version}}', releaseInfo?.version || '')}
|
|
||||||
</p>
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<ScrollShadow className="max-h-[450px]" hideScrollBar>
|
|
||||||
<div className="markdown rounded-lg bg-default-50 p-4">
|
|
||||||
<Markdown>
|
|
||||||
{typeof releaseNotes === 'string'
|
|
||||||
? releaseNotes
|
|
||||||
: Array.isArray(releaseNotes)
|
|
||||||
? releaseNotes
|
|
||||||
.map((note: ReleaseNoteInfo) => note.note)
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n\n')
|
|
||||||
: t('update.noReleaseNotes')}
|
|
||||||
</Markdown>
|
|
||||||
</div>
|
|
||||||
</ScrollShadow>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button variant="light" onPress={onModalClose} isDisabled={isInstalling}>
|
|
||||||
{t('update.later')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
onPress={async () => {
|
|
||||||
await handleInstall()
|
|
||||||
onModalClose()
|
|
||||||
}}
|
|
||||||
isLoading={isInstalling}>
|
|
||||||
{t('update.install')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UpdateDialog
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { SyncOutlined } from '@ant-design/icons'
|
import { SyncOutlined } from '@ant-design/icons'
|
||||||
import { useDisclosure } from '@heroui/react'
|
import UpdateDialogPopup from '@renderer/components/Popups/UpdateDialogPopup'
|
||||||
import UpdateDialog from '@renderer/components/UpdateDialog'
|
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
@ -12,7 +11,6 @@ const UpdateAppButton: FC = () => {
|
|||||||
const { update } = useRuntime()
|
const { update } = useRuntime()
|
||||||
const { autoCheckUpdate } = useSettings()
|
const { autoCheckUpdate } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
|
||||||
|
|
||||||
if (!update) {
|
if (!update) {
|
||||||
return null
|
return null
|
||||||
@ -22,19 +20,21 @@ const UpdateAppButton: FC = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOpenUpdateDialog = () => {
|
||||||
|
UpdateDialogPopup.show({ releaseInfo: update.info || null })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<UpdateButton
|
<UpdateButton
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
onClick={onOpen}
|
onClick={handleOpenUpdateDialog}
|
||||||
icon={<SyncOutlined />}
|
icon={<SyncOutlined />}
|
||||||
color="orange"
|
color="orange"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small">
|
size="small">
|
||||||
{t('button.update_available')}
|
{t('button.update_available')}
|
||||||
</UpdateButton>
|
</UpdateButton>
|
||||||
|
|
||||||
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={update.info || null} />
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { GithubOutlined } from '@ant-design/icons'
|
import { GithubOutlined } from '@ant-design/icons'
|
||||||
import { useDisclosure } from '@heroui/react'
|
|
||||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import UpdateDialog from '@renderer/components/UpdateDialog'
|
import UpdateDialogPopup from '@renderer/components/Popups/UpdateDialogPopup'
|
||||||
import { APP_NAME, AppLogo } from '@renderer/config/env'
|
import { APP_NAME, AppLogo } from '@renderer/config/env'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
@ -15,7 +14,6 @@ import { ThemeMode } from '@renderer/types'
|
|||||||
import { runAsyncFunction } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
import { UpgradeChannel } from '@shared/config/constant'
|
import { UpgradeChannel } from '@shared/config/constant'
|
||||||
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
|
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
|
||||||
import type { UpdateInfo } from 'builder-util-runtime'
|
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
|
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
|
||||||
import { BadgeQuestionMark } from 'lucide-react'
|
import { BadgeQuestionMark } from 'lucide-react'
|
||||||
@ -31,8 +29,6 @@ import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitl
|
|||||||
const AboutSettings: FC = () => {
|
const AboutSettings: FC = () => {
|
||||||
const [version, setVersion] = useState('')
|
const [version, setVersion] = useState('')
|
||||||
const [isPortable, setIsPortable] = useState(false)
|
const [isPortable, setIsPortable] = useState(false)
|
||||||
const [updateDialogInfo, setUpdateDialogInfo] = useState<UpdateInfo | null>(null)
|
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings()
|
const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
@ -48,8 +44,7 @@ const AboutSettings: FC = () => {
|
|||||||
|
|
||||||
if (update.downloaded) {
|
if (update.downloaded) {
|
||||||
// Open update dialog directly in renderer
|
// Open update dialog directly in renderer
|
||||||
setUpdateDialogInfo(update.info || null)
|
UpdateDialogPopup.show({ releaseInfo: update.info || null })
|
||||||
onOpen()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,9 +337,6 @@ const AboutSettings: FC = () => {
|
|||||||
<Button onClick={debug}>{t('settings.about.debug.open')}</Button>
|
<Button onClick={debug}>{t('settings.about.debug.open')}</Button>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
|
|
||||||
{/* Update Dialog */}
|
|
||||||
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={updateDialogInfo} />
|
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user