refactor: replace UpdateDialog with UpdateDialogPopup for better modal handling

This commit is contained in:
kangfenmao 2025-11-05 11:40:47 +08:00
parent 468aebd632
commit b334a2c5be
4 changed files with 213 additions and 117 deletions

View 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;
}
}
`

View File

@ -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

View File

@ -1,6 +1,5 @@
import { SyncOutlined } from '@ant-design/icons'
import { useDisclosure } from '@heroui/react'
import UpdateDialog from '@renderer/components/UpdateDialog'
import UpdateDialogPopup from '@renderer/components/Popups/UpdateDialogPopup'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { Button } from 'antd'
@ -12,7 +11,6 @@ const UpdateAppButton: FC = () => {
const { update } = useRuntime()
const { autoCheckUpdate } = useSettings()
const { t } = useTranslation()
const { isOpen, onOpen, onClose } = useDisclosure()
if (!update) {
return null
@ -22,19 +20,21 @@ const UpdateAppButton: FC = () => {
return null
}
const handleOpenUpdateDialog = () => {
UpdateDialogPopup.show({ releaseInfo: update.info || null })
}
return (
<Container>
<UpdateButton
className="nodrag"
onClick={onOpen}
onClick={handleOpenUpdateDialog}
icon={<SyncOutlined />}
color="orange"
variant="outlined"
size="small">
{t('button.update_available')}
</UpdateButton>
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={update.info || null} />
</Container>
)
}

View File

@ -1,8 +1,7 @@
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 UpdateDialogPopup from '@renderer/components/Popups/UpdateDialogPopup'
import { APP_NAME, AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
@ -15,7 +14,6 @@ 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 type { UpdateInfo } from 'builder-util-runtime'
import { debounce } from 'lodash'
import { Bug, Building2, Github, Globe, Mail, Rss } from 'lucide-react'
import { BadgeQuestionMark } from 'lucide-react'
@ -31,8 +29,6 @@ import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitl
const AboutSettings: FC = () => {
const [version, setVersion] = useState('')
const [isPortable, setIsPortable] = useState(false)
const [updateDialogInfo, setUpdateDialogInfo] = useState<UpdateInfo | null>(null)
const { isOpen, onOpen, onClose } = useDisclosure()
const { t } = useTranslation()
const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings()
const { theme } = useTheme()
@ -48,8 +44,7 @@ const AboutSettings: FC = () => {
if (update.downloaded) {
// Open update dialog directly in renderer
setUpdateDialogInfo(update.info || null)
onOpen()
UpdateDialogPopup.show({ releaseInfo: update.info || null })
return
}
@ -342,9 +337,6 @@ const AboutSettings: FC = () => {
<Button onClick={debug}>{t('settings.about.debug.open')}</Button>
</SettingRow>
</SettingGroup>
{/* Update Dialog */}
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={updateDialogInfo} />
</SettingContainer>
)
}