Compare commits

...

2 Commits

2 changed files with 108 additions and 35 deletions

View File

@ -4,6 +4,7 @@ import { Button, Flex, Modal, Progress, Typography, theme } from 'antd'
import CloudIcon from '../../../Icons/CloudIcon' import CloudIcon from '../../../Icons/CloudIcon'
import HostIcon from '../../../Icons/HostIcon' import HostIcon from '../../../Icons/HostIcon'
import ReloadIcon from '../../../Icons/ReloadIcon'
import CheckCircleIcon from '../../../Icons/CheckCircleIcon' import CheckCircleIcon from '../../../Icons/CheckCircleIcon'
import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon' import XMarkCircleIcon from '../../../Icons/XMarkCircleIcon'
@ -45,6 +46,15 @@ const STAGE_CONFIG = {
complete: 'Installed', complete: 'Installed',
error: 'Install failed' error: 'Install failed'
} }
},
restart: {
icon: ReloadIcon,
labels: {
pending: 'Restart',
active: 'Restarting...',
complete: 'Restarted',
error: 'Restart failed'
}
} }
} }
@ -62,20 +72,25 @@ const getDownloadStageStatus = (phase, isError) => {
return 'pending' return 'pending'
} }
const isInstallComplete = (phase, message) =>
phase === 'installing' &&
String(message || '')
.toLowerCase()
.includes('complete')
const getInstallStageStatus = (phase, isError, message) => { const getInstallStageStatus = (phase, isError, message) => {
if (isError && ['downloaded', 'installing'].includes(phase)) return 'error' if (isError && ['downloaded', 'installing'].includes(phase)) return 'error'
if ( if (isInstallComplete(phase, message)) return 'complete'
phase === 'installing' &&
String(message || '')
.toLowerCase()
.includes('complete')
) {
return 'complete'
}
if (phase === 'installing') return 'active' if (phase === 'installing') return 'active'
return 'pending' return 'pending'
} }
const getRestartStageStatus = (phase, isError, message) => {
if (isError && isInstallComplete(phase, message)) return 'error'
if (isInstallComplete(phase, message)) return 'active'
return 'pending'
}
const getProgressStatus = (stageStatus) => { const getProgressStatus = (stageStatus) => {
if (stageStatus === 'error') return 'exception' if (stageStatus === 'error') return 'exception'
if (stageStatus === 'complete') return 'success' if (stageStatus === 'complete') return 'success'
@ -101,25 +116,19 @@ const UpdateStage = ({ stage, status, percent, detail }) => {
: StageIcon : StageIcon
return ( return (
<Flex align='center' gap='middle'> <Flex align='start' gap='middle' style={{ width: '100%' }}>
<StatusIcon style={{ fontSize: 20, color, flexShrink: 0 }} /> <StatusIcon style={{ fontSize: 22, color, flexShrink: 0 }} />
<Flex align='center' gap='small' style={{ flex: 1, minWidth: 0 }}> <Flex align='start' gap='24px' style={{ flex: 1, minWidth: 0 }}>
<Text style={{ flexShrink: 0, minWidth: 96 }}> <Text style={{ flexShrink: 0 }}>{config.labels[resolvedStatus]}</Text>
{config.labels[resolvedStatus]}
</Text>
{showProgress && ( {showProgress && (
<Flex vertical gap={4}> <Flex vertical gap={2} style={{ flex: 1 }}>
<Progress <Progress
percent={resolvedPercent} percent={resolvedPercent}
status={getProgressStatus(resolvedStatus)} status={getProgressStatus(resolvedStatus)}
showInfo={typeof resolvedPercent === 'number'} showInfo={typeof resolvedPercent === 'number'}
style={{ flex: 1, margin: 0 }} style={{ flex: 1, margin: 0 }}
/> />
{detail && ( {detail && <Text type='secondary'>{detail}</Text>}
<Text type='secondary' style={{ marginLeft: 36 }}>
{detail}
</Text>
)}
</Flex> </Flex>
)} )}
</Flex> </Flex>
@ -128,7 +137,7 @@ const UpdateStage = ({ stage, status, percent, detail }) => {
} }
UpdateStage.propTypes = { UpdateStage.propTypes = {
stage: PropTypes.oneOf(['download', 'install']).isRequired, stage: PropTypes.oneOf(['download', 'install', 'restart']).isRequired,
status: PropTypes.oneOf(['pending', 'active', 'complete', 'error']) status: PropTypes.oneOf(['pending', 'active', 'complete', 'error'])
.isRequired, .isRequired,
percent: PropTypes.number, percent: PropTypes.number,
@ -150,6 +159,7 @@ const AppUpdateProgress = ({ progress, update, onClose }) => {
const downloadStatus = getDownloadStageStatus(phase, isError) const downloadStatus = getDownloadStageStatus(phase, isError)
const installStatus = getInstallStageStatus(phase, isError, message) const installStatus = getInstallStageStatus(phase, isError, message)
const restartStatus = getRestartStageStatus(phase, isError, message)
const downloadPercent = const downloadPercent =
downloadStatus === 'active' ? (phase === 'preparing' ? 0 : percent) : null downloadStatus === 'active' ? (phase === 'preparing' ? 0 : percent) : null
@ -163,6 +173,8 @@ const AppUpdateProgress = ({ progress, update, onClose }) => {
const installDetail = installStatus === 'active' ? message : null const installDetail = installStatus === 'active' ? message : null
const restartDetail = restartStatus === 'active' ? message : null
return ( return (
<Flex vertical gap='middle'> <Flex vertical gap='middle'>
<Text> <Text>
@ -185,6 +197,11 @@ const AppUpdateProgress = ({ progress, update, onClose }) => {
percent={installPercent} percent={installPercent}
detail={installDetail} detail={installDetail}
/> />
<UpdateStage
stage='restart'
status={restartStatus}
detail={restartDetail}
/>
</Flex> </Flex>
<Modal <Modal

View File

@ -21,6 +21,42 @@ const { Text } = Typography
const UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000 const UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000
const DEFAULT_UPDATE_BRANCH = 'main' const DEFAULT_UPDATE_BRANCH = 'main'
const CURRENT_BUILD_NUMBER = import.meta.env.VITE_BUILD_NUMBER const CURRENT_BUILD_NUMBER = import.meta.env.VITE_BUILD_NUMBER
const APP_UPDATE_DISMISSED_KEY = 'appUpdateDismissed'
const getDismissedUpdate = () => {
try {
const stored = sessionStorage.getItem(APP_UPDATE_DISMISSED_KEY)
return stored ? JSON.parse(stored) : null
} catch {
return null
}
}
const isUpdateDismissed = (update) => {
if (!update) return false
const dismissed = getDismissedUpdate()
if (!dismissed) return false
return (
dismissed.version === update.version &&
dismissed.buildNumber === update.buildNumber &&
dismissed.branch === update.branch
)
}
const saveDismissedUpdate = (update) => {
if (!update) return
sessionStorage.setItem(
APP_UPDATE_DISMISSED_KEY,
JSON.stringify({
version: update.version,
buildNumber: update.buildNumber,
branch: update.branch
})
)
}
const AppUpdateContext = createContext() const AppUpdateContext = createContext()
@ -77,6 +113,7 @@ export const AppUpdateProvider = ({ children }) => {
const [checking, setChecking] = useState(false) const [checking, setChecking] = useState(false)
const [noUpdateOpen, setNoUpdateOpen] = useState(false) const [noUpdateOpen, setNoUpdateOpen] = useState(false)
const [availableUpdate, setAvailableUpdate] = useState(null) const [availableUpdate, setAvailableUpdate] = useState(null)
const [updatePromptOpen, setUpdatePromptOpen] = useState(false)
const [installingUpdate, setInstallingUpdate] = useState(null) const [installingUpdate, setInstallingUpdate] = useState(null)
const [updateProgress, setUpdateProgress] = useState(null) const [updateProgress, setUpdateProgress] = useState(null)
const runningCheckRef = useRef(null) const runningCheckRef = useRef(null)
@ -136,14 +173,20 @@ export const AppUpdateProvider = ({ children }) => {
} }
}, []) }, [])
const showUpdateIfAvailable = useCallback(async () => { const showUpdateIfAvailable = useCallback(
const update = await checkForAvailableUpdate() async ({ forcePrompt = false } = {}) => {
if (update) { const update = await checkForAvailableUpdate()
setNoUpdateOpen(false) if (update) {
setAvailableUpdate(update) setNoUpdateOpen(false)
} setAvailableUpdate(update)
return update if (forcePrompt || !isUpdateDismissed(update)) {
}, [checkForAvailableUpdate]) setUpdatePromptOpen(true)
}
}
return update
},
[checkForAvailableUpdate]
)
const checkForUpdates = useCallback(async () => { const checkForUpdates = useCallback(async () => {
if (!isElectron) return null if (!isElectron) return null
@ -151,7 +194,7 @@ export const AppUpdateProvider = ({ children }) => {
setChecking(true) setChecking(true)
try { try {
const update = await showUpdateIfAvailable() const update = await showUpdateIfAvailable({ forcePrompt: true })
if (!update) { if (!update) {
setNoUpdateOpen(true) setNoUpdateOpen(true)
} }
@ -183,15 +226,22 @@ export const AppUpdateProvider = ({ children }) => {
}) })
}, [isElectron, onAppUpdateProgress]) }, [isElectron, onAppUpdateProgress])
const dismissUpdatePrompt = () => {
if (availableUpdate) {
saveDismissedUpdate(availableUpdate)
}
setUpdatePromptOpen(false)
}
const closeUpdateModal = () => { const closeUpdateModal = () => {
setAvailableUpdate(null) setUpdatePromptOpen(false)
setInstallingUpdate(null) setInstallingUpdate(null)
setUpdateProgress(null) setUpdateProgress(null)
} }
const handleUpdate = async (update) => { const handleUpdate = async (update) => {
setNoUpdateOpen(false) setNoUpdateOpen(false)
setAvailableUpdate(null) setUpdatePromptOpen(false)
setInstallingUpdate(update) setInstallingUpdate(update)
setModelWidth(550) setModelWidth(550)
setUpdateProgress({ setUpdateProgress({
@ -215,7 +265,7 @@ export const AppUpdateProvider = ({ children }) => {
} }
} }
const updateModalOpen = Boolean(availableUpdate || installingUpdate) const updateModalOpen = Boolean(updatePromptOpen || installingUpdate)
const updateModalBusy = const updateModalBusy =
Boolean(installingUpdate) && updateProgress?.phase !== 'error' Boolean(installingUpdate) && updateProgress?.phase !== 'error'
@ -268,7 +318,13 @@ export const AppUpdateProvider = ({ children }) => {
centered centered
closable={!updateModalBusy} closable={!updateModalBusy}
maskClosable={!updateModalBusy} maskClosable={!updateModalBusy}
onCancel={updateModalBusy ? undefined : closeUpdateModal} onCancel={
updateModalBusy
? undefined
: installingUpdate
? closeUpdateModal
: dismissUpdatePrompt
}
> >
{installingUpdate ? ( {installingUpdate ? (
<AppUpdateProgress <AppUpdateProgress
@ -279,7 +335,7 @@ export const AppUpdateProvider = ({ children }) => {
) : ( ) : (
<NewAppUpdate <NewAppUpdate
update={availableUpdate} update={availableUpdate}
onCancel={closeUpdateModal} onCancel={dismissUpdatePrompt}
onUpdate={handleUpdate} onUpdate={handleUpdate}
/> />
)} )}