import { useStore } from '@nanostores/react' import { useEffect, useState } from 'react' import { BrandMark } from '@/components/brand-mark' import { Button } from '@/components/ui/button' import { writeClipboardText } from '@/components/ui/copy-button' import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog' import { ErrorIcon, ErrorState } from '@/components/ui/error-state' import { Loader } from '@/components/ui/loader' import type { DesktopUpdateCommit, DesktopUpdateStage, DesktopUpdateStatus } from '@/global' import { useI18n } from '@/i18n' import { buildCommitChangelog, type CommitGroup } from '@/lib/commit-changelog' import { AlertCircle, Check, CheckCircle2, Copy, Terminal } from '@/lib/icons' import { cn } from '@/lib/utils' import { $updateApply, $updateChecking, $updateOverlayOpen, $updateStatus, applyUpdates, checkUpdates, resetUpdateApplyState, setUpdateOverlayOpen, type UpdateApplyState } from '@/store/updates' function totalItems(groups: readonly CommitGroup[]) { return groups.reduce((sum, g) => sum + g.items.length, 0) } export function UpdatesOverlay() { const open = useStore($updateOverlayOpen) const status = useStore($updateStatus) const checking = useStore($updateChecking) const apply = useStore($updateApply) useEffect(() => { if (open && !status && !checking) { void checkUpdates() } }, [checking, open, status]) const behind = status?.behind ?? 0 const phase: 'idle' | 'applying' | 'manual' | 'error' = apply.stage === 'manual' ? 'manual' : apply.applying || apply.stage === 'restart' ? 'applying' : apply.stage === 'error' ? 'error' : 'idle' const handleClose = (next: boolean) => { if (phase === 'applying') { return } setUpdateOverlayOpen(next) if (!next && (apply.stage === 'error' || apply.stage === 'restart' || apply.stage === 'manual')) { resetUpdateApplyState() } } const handleInstall = () => { void applyUpdates() } return ( {phase === 'applying' && } {phase === 'manual' && ( handleClose(false)} /> )} {phase === 'error' && ( handleClose(false)} onRetry={handleInstall} /> )} {phase === 'idle' && ( handleClose(false)} onRetryCheck={() => void checkUpdates()} status={status} /> )} ) } function IdleView({ behind, checking, commits, onInstall, onLater, onRetryCheck, status }: { behind: number checking: boolean commits: readonly DesktopUpdateCommit[] onInstall: () => void onLater: () => void onRetryCheck: () => void status: DesktopUpdateStatus | null }) { const { t } = useI18n() const u = t.updates if (!status && checking) { return ( } title={u.checking} /> ) } if (!status) { return ( {u.tryAgain} } icon={} title={u.checkFailedTitle} /> ) } if (!status.supported) { return ( } title={u.notAvailableTitle} /> ) } if (status.error) { return ( {u.tryAgain} } body={u.connectionRetry} icon={} title={u.checkFailedTitle} /> ) } if (behind === 0) { return ( } title={u.allSetTitle} /> ) } const groups = buildCommitChangelog(commits) const shownItems = totalItems(groups) const remaining = Math.max(0, behind - shownItems) return ( {u.availableTitle} {u.availableBody} {groups.map(group => ( {group.label} {group.items.map(item => ( {item} ))} ))} {u.updateNow} {u.maybeLater} {remaining > 0 && ( {u.moreChanges(remaining)} )} ) } function ManualView({ command, onDone }: { command: string; onDone: () => void }) { const { t } = useI18n() const u = t.updates const [copied, setCopied] = useState(false) const handleCopy = () => { void writeClipboardText(command).then(() => { setCopied(true) window.setTimeout(() => setCopied(false), 1800) }) } return ( {u.manualTitle} {u.manualBody} $ {command} {copied ? ( <> {u.copied} > ) : ( <> {u.copy} > )} {u.manualPickedUp} {u.done} ) } function ApplyingView({ apply }: { apply: UpdateApplyState }) { const { t } = useI18n() const u = t.updates const label = u.stages[apply.stage as DesktopUpdateStage] ?? u.stages.idle const percent = typeof apply.percent === 'number' && Number.isFinite(apply.percent) ? Math.max(2, Math.min(100, Math.round(apply.percent))) : null return ( {label} {u.applyingBody} {u.applyingClose} ) } function ErrorView({ message, onDismiss, onRetry }: { message: string; onDismiss: () => void; onRetry: () => void }) { const { t } = useI18n() const u = t.updates return ( {message || u.errorBody} } title={ {u.errorTitle} } > {u.tryAgain} {u.notNow} ) } function CenteredStatus({ action, body, icon, title }: { action?: React.ReactNode body?: string icon: React.ReactNode title: string }) { return ( {icon} {title} {body && {body}} {action && {action}} ) }
{group.label}
{u.moreChanges(remaining)}
$ {command}
{u.manualPickedUp}
{u.applyingClose}