codev / desktop /src /components /shared /UpdateChecker.tsx
chenbhao's picture
feat: desktop
1f21206
Raw
History Blame
4.38 kB
import { useEffect } from 'react'
import { useTranslation } from '../../i18n'
import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
import { isTauriRuntime } from '../../lib/desktopRuntime'
import { useUpdateStore } from '../../stores/updateStore'
import { formatBytes } from '../../lib/formatBytes'
export function UpdateChecker() {
const t = useTranslation()
const status = useUpdateStore((s) => s.status)
const availableVersion = useUpdateStore((s) => s.availableVersion)
const releaseNotes = useUpdateStore((s) => s.releaseNotes)
const progressPercent = useUpdateStore((s) => s.progressPercent)
const downloadedBytes = useUpdateStore((s) => s.downloadedBytes)
const totalBytes = useUpdateStore((s) => s.totalBytes)
const error = useUpdateStore((s) => s.error)
const shouldPrompt = useUpdateStore((s) => s.shouldPrompt)
const initialize = useUpdateStore((s) => s.initialize)
const installUpdate = useUpdateStore((s) => s.installUpdate)
const dismissPrompt = useUpdateStore((s) => s.dismissPrompt)
useEffect(() => {
void initialize()
}, [initialize])
if (!isTauriRuntime()) return null
const showPopup =
shouldPrompt && !!availableVersion && ['available', 'downloading', 'restarting'].includes(status)
if (!showPopup) return null
const hasKnownProgress = typeof totalBytes === 'number' && totalBytes > 0
const downloadedText = formatBytes(downloadedBytes)
const statusText =
status === 'restarting'
? t('update.restarting')
: status === 'downloading'
? hasKnownProgress
? t('update.downloading')
: t('update.progressBytes', { downloaded: downloadedText })
: null
return (
<div className="fixed top-4 right-4 z-[200] max-w-sm">
<div className="bg-[var(--color-surface-container-low)] border border-[var(--color-border)] rounded-[var(--radius-lg)] shadow-[var(--shadow-dropdown)] p-4">
<p className="text-sm font-medium text-[var(--color-text-primary)]">
{t('update.available', { version: availableVersion })}
</p>
{releaseNotes && (
<div className="mt-2 max-h-40 overflow-y-auto rounded-lg border border-[var(--color-border)]/60 bg-[var(--color-surface)]/70 px-3 py-2">
<MarkdownRenderer
content={releaseNotes}
className="text-xs leading-5 text-[var(--color-text-secondary)] [&_h1]:mb-2 [&_h1]:text-sm [&_h1]:font-semibold [&_h2]:mb-1.5 [&_h2]:text-xs [&_h2]:font-semibold [&_p]:my-1.5 [&_p]:text-xs [&_p]:leading-5 [&_ul]:my-1.5 [&_ol]:my-1.5"
/>
</div>
)}
{(status === 'downloading' || status === 'restarting') && (
<div className="mt-3">
<div className="h-1.5 bg-[var(--color-surface)] rounded-full overflow-hidden">
{hasKnownProgress || status === 'restarting' ? (
<div
className="h-full bg-[var(--color-text-accent)] transition-all duration-300"
style={{ width: `${Math.min(progressPercent, 100)}%` }}
/>
) : (
<div className="h-full w-1/3 rounded-full bg-[var(--color-text-accent)]/75 animate-pulse" />
)}
</div>
{statusText && (
<p className="text-xs text-[var(--color-text-tertiary)] mt-1">
{statusText}
{status === 'downloading' && hasKnownProgress ? ` ${progressPercent}%` : ''}
</p>
)}
</div>
)}
{error && (
<p className="mt-2 text-xs text-[var(--color-error)]">
{t('update.failed', { error })}
</p>
)}
{status === 'available' && (
<div className="mt-3 flex gap-2">
<button
onClick={() => void installUpdate()}
className="px-3 py-1 text-xs font-medium rounded-[var(--radius-md)] bg-[var(--color-text-accent)] text-white hover:opacity-90 transition-opacity"
>
{t('update.now')}
</button>
<button
onClick={dismissPrompt}
className="px-3 py-1 text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] transition-colors"
>
{t('update.later')}
</button>
</div>
)}
</div>
</div>
)
}