/** * Update Settings Component * Displays update status and allows manual update checking/installation */ import { useEffect, useCallback } from 'react'; import { Download, RefreshCw, Loader2, Rocket, XCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; import { useUpdateStore } from '@/stores/update'; import { useTranslation } from 'react-i18next'; function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } function formatUpdateError(error: string | null, fallback: string): string { if (!error) return fallback; const firstLine = error.replace(/\\r\\n|\\n|\r\n|\n/g, '\n').split('\n', 1)[0].trim(); const isTechnical = /(?:^\w*Error\s*:|HttpError|Cannot find channel|node_modules|https?:\/\/|\bnet::ERR_[A-Z0-9_]+\b|\bE(?:ACCES|CONNREFUSED|CONNRESET|EXIST|HOSTUNREACH|ISDIR|NETUNREACH|NOENT|NOTDIR|PERM|PIPE|TIMEDOUT)\b|\b(?:ERR|UPDATER)_[A-Z0-9_]+\b|[A-Z]:\\|\\\\[^\\]|\bat\s+\S+\s*\()/i.test( firstLine, ) || /^[{[]/.test(firstLine); const isUserFacingProse = /\p{Script=Han}/u.test(firstLine); if (!firstLine || firstLine.length > 180 || isTechnical || !isUserFacingProse) return fallback; return firstLine; } export function UpdateSettings() { const { t } = useTranslation('settings'); const { status, currentVersion, updateInfo, progress, error, isInitialized, autoInstallCountdown, init, checkForUpdates, downloadUpdate, installUpdate, cancelAutoInstall, clearError, } = useUpdateStore(); // Initialize on mount useEffect(() => { init(); }, [init]); const handleCheckForUpdates = useCallback(async () => { clearError(); await checkForUpdates(); }, [checkForUpdates, clearError]); const renderStatusIcon = () => { switch (status) { case 'checking': case 'downloading': return ; case 'available': return ; case 'downloaded': return ; case 'error': return ; default: return ; } }; const renderStatusText = () => { if (status === 'downloaded' && autoInstallCountdown != null && autoInstallCountdown >= 0) { return t('updates.status.autoInstalling', { seconds: autoInstallCountdown }); } switch (status) { case 'checking': return t('updates.status.checking'); case 'downloading': return t('updates.status.downloading'); case 'available': return t('updates.status.available', { version: updateInfo?.version }); case 'downloaded': return t('updates.status.downloaded', { version: updateInfo?.version }); case 'error': return formatUpdateError(error, t('updates.status.failed')); case 'not-available': return t('updates.status.latest'); default: return t('updates.status.check'); } }; const renderAction = () => { switch (status) { case 'checking': return ( ); case 'downloading': return ( ); case 'available': return ( ); case 'downloaded': if (autoInstallCountdown != null && autoInstallCountdown >= 0) { return ( ); } return ( ); case 'error': return ( ); default: return ( ); } }; if (!isInitialized) { return (
Loading...
); } return (
{/* Current Version */}

{t('updates.currentVersion')}

v{currentVersion}

{renderStatusIcon()}
{/* Status */}

{renderStatusText()}

{renderAction()}
{/* Download Progress */} {status === 'downloading' && progress && (
{formatBytes(progress.transferred)} / {formatBytes(progress.total)} {formatBytes(progress.bytesPerSecond)}/s

{Math.round(progress.percent)}% complete

)} {/* Update Info */} {updateInfo && (status === 'available' || status === 'downloaded') && (

Version {updateInfo.version}

{updateInfo.releaseDate && (

{new Date(updateInfo.releaseDate).toLocaleDateString()}

)}
{updateInfo.releaseNotes && (

{t('updates.whatsNew')}

{updateInfo.releaseNotes}

)}
)}
); } export default UpdateSettings;