Files
makelore/src/components/settings/UpdateSettings.tsx
inman 26b52d76e3
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
feat: 完善图像工作区与创作工具体验
2026-08-16 14:08:27 +08:00

224 lines
7.4 KiB
TypeScript

/**
* 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 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />;
case 'available':
return <Download className="h-4 w-4 text-primary" />;
case 'downloaded':
return <Rocket className="h-4 w-4 text-primary" />;
case 'error':
return <RefreshCw className="h-4 w-4 text-destructive" />;
default:
return <RefreshCw className="h-4 w-4 text-muted-foreground" />;
}
};
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 (
<Button disabled variant="outline" size="sm">
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{t('updates.action.checking')}
</Button>
);
case 'downloading':
return (
<Button disabled variant="outline" size="sm">
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{t('updates.action.downloading')}
</Button>
);
case 'available':
return (
<Button onClick={downloadUpdate} size="sm">
<Download className="h-4 w-4 mr-2" />
{t('updates.action.download')}
</Button>
);
case 'downloaded':
if (autoInstallCountdown != null && autoInstallCountdown >= 0) {
return (
<Button onClick={cancelAutoInstall} size="sm" variant="outline">
<XCircle className="h-4 w-4 mr-2" />
{t('updates.action.cancelAutoInstall')}
</Button>
);
}
return (
<Button onClick={installUpdate} size="sm" variant="default">
<Rocket className="h-4 w-4 mr-2" />
{t('updates.action.install')}
</Button>
);
case 'error':
return (
<Button onClick={handleCheckForUpdates} variant="outline" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
{t('updates.action.retry')}
</Button>
);
default:
return (
<Button onClick={handleCheckForUpdates} variant="outline" size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
{t('updates.action.check')}
</Button>
);
}
};
if (!isInitialized) {
return (
<div className="flex items-center gap-2 text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
<span>Loading...</span>
</div>
);
}
return (
<div className="space-y-4">
{/* Current Version */}
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">{t('updates.currentVersion')}</p>
<p className="text-2xl font-medium">v{currentVersion}</p>
</div>
{renderStatusIcon()}
</div>
{/* Status */}
<div className="flex items-center justify-between py-3 border-t border-b">
<p className="min-w-0 flex-1 break-words pr-3 text-sm text-muted-foreground">
{renderStatusText()}
</p>
{renderAction()}
</div>
{/* Download Progress */}
{status === 'downloading' && progress && (
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span>
{formatBytes(progress.transferred)} / {formatBytes(progress.total)}
</span>
<span>{formatBytes(progress.bytesPerSecond)}/s</span>
</div>
<Progress value={progress.percent} className="h-2" />
<p className="text-xs text-muted-foreground text-center">
{Math.round(progress.percent)}% complete
</p>
</div>
)}
{/* Update Info */}
{updateInfo && (status === 'available' || status === 'downloaded') && (
<div className="rounded-lg bg-muted p-4 space-y-2">
<div className="flex items-center justify-between">
<p className="font-medium">Version {updateInfo.version}</p>
{updateInfo.releaseDate && (
<p className="text-sm text-muted-foreground">
{new Date(updateInfo.releaseDate).toLocaleDateString()}
</p>
)}
</div>
{updateInfo.releaseNotes && (
<div className="text-sm text-muted-foreground prose prose-sm max-w-none">
<p className="font-medium text-foreground mb-1">{t('updates.whatsNew')}</p>
<p className="whitespace-pre-wrap">{updateInfo.releaseNotes}</p>
</div>
)}
</div>
)}
</div>
);
}
export default UpdateSettings;