Files

262 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState, useEffect } from 'react';
import { useI18n } from '../lib/useI18n';
export default function UpdateBanner() {
const { t } = useI18n();
const [updateInfo, setUpdateInfo] = useState(null);
const [dismissed, setDismissed] = useState(false);
const [updating, setUpdating] = useState(false);
const [updateResult, setUpdateResult] = useState(null); // { success, message, logs }
const [downloadProgress, setDownloadProgress] = useState(null); // { progress, downloaded, total }
const [sourceProgress, setSourceProgress] = useState(null); // { step, total, label, status }
const isElectron = typeof window !== 'undefined' && window.electronAPI?.isElectron;
useEffect(() => {
const checkUpdate = async () => {
try {
const res = await fetch('/api/check-update', { cache: 'no-store' });
if (!res.ok) return;
const data = await res.json();
if (data.hasUpdate && data.latest) {
const dismissedVersion = sessionStorage.getItem('author-update-dismissed');
if (dismissedVersion === data.latest) return;
setUpdateInfo(data);
}
} catch {
// 网络失败静默跳过
}
};
const timer = setTimeout(checkUpdate, 3000);
return () => clearTimeout(timer);
}, []);
// 监听 Electron 下载进度
useEffect(() => {
if (isElectron && window.electronAPI?.onUpdateProgress) {
window.electronAPI.onUpdateProgress((data) => {
setDownloadProgress(data);
});
}
}, [isElectron]);
const handleDismiss = () => {
setDismissed(true);
if (updateInfo?.latest) {
sessionStorage.setItem('author-update-dismissed', updateInfo.latest);
}
};
// Electron 客户端:自动下载安装
const handleElectronUpdate = async () => {
setUpdating(true);
setUpdateResult(null);
setDownloadProgress({ progress: 0, downloaded: 0, total: 0 });
try {
const result = await window.electronAPI.downloadAndInstallUpdate();
if (!result.success) {
setUpdateResult({ success: false, message: t('update.updateFailed') + ': ' + (result.error || '') });
setDownloadProgress(null);
}
// 成功时 app 会自动退出,不需要处理
} catch (err) {
setUpdateResult({ success: false, message: t('update.updateFailed') + ': ' + err.message });
setDownloadProgress(null);
} finally {
setUpdating(false);
}
};
// 源码部署:SSE 流式更新
const handleSourceUpdate = async () => {
setUpdating(true);
setUpdateResult(null);
setSourceProgress(null);
try {
const res = await fetch('/api/update-source-stream', { method: 'POST' });
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const block of lines) {
const dataLine = block.split('\n').find(l => l.startsWith('data: '));
if (!dataLine) continue;
const data = JSON.parse(dataLine.slice(6));
if (data.done) {
if (data.success) {
if (data.alreadyUpToDate) {
setUpdateResult({ success: true, message: t('update.alreadyLatest') });
} else {
setUpdateResult({ success: true, message: t('update.updateSuccess') });
}
} else {
setUpdateResult({ success: false, message: t('update.updateFailed') + ': ' + (data.error || '') });
}
setSourceProgress(null);
} else {
setSourceProgress(data);
}
}
}
} catch (err) {
setUpdateResult({ success: false, message: t('update.updateFailed') + ': ' + err.message });
setSourceProgress(null);
} finally {
setUpdating(false);
}
};
const handleUpdate = () => {
if (isElectron) {
handleElectronUpdate();
} else if (updateInfo?.isSourceDeploy) {
handleSourceUpdate();
}
};
if (!updateInfo || dismissed) return null;
const versionText = t('update.newVersion').replace('{version}', `v${updateInfo.latest}`);
const canAutoUpdate = isElectron || updateInfo.isSourceDeploy;
return (
<div className="update-banner">
<div className="update-banner-content">
<span className="update-banner-icon">🔔</span>
<span className="update-banner-text">{versionText}</span>
{/* 一键更新(Electron 或 源码部署) */}
{canAutoUpdate && !updateResult && (
<button
className="update-banner-link"
onClick={handleUpdate}
disabled={updating}
style={{
background: 'rgba(255,255,255,0.2)', border: '1px solid rgba(255,255,255,0.4)',
borderRadius: 6, padding: '3px 12px', cursor: updating ? 'wait' : 'pointer',
fontWeight: 700, transition: 'all 0.15s',
}}
>
{updating
? (downloadProgress
? `⬇️ ${downloadProgress.progress}%`
: sourceProgress
? `${sourceProgress.label} (${sourceProgress.step}/${sourceProgress.total})`
: t('update.updating'))
: t('update.updateNow')
}
</button>
)}
{/* 下载进度条(Electron) */}
{updating && downloadProgress && downloadProgress.total > 0 && (
<div style={{
width: 120, height: 6, background: 'rgba(255,255,255,0.2)',
borderRadius: 3, overflow: 'hidden', flexShrink: 0,
}}>
<div style={{
width: `${downloadProgress.progress}%`, height: '100%',
background: '#a7f3d0', borderRadius: 3,
transition: 'width 0.3s ease',
}} />
</div>
)}
{/* 源码更新进度条 */}
{updating && sourceProgress && (
<div style={{
width: 120, height: 6, background: 'rgba(255,255,255,0.2)',
borderRadius: 3, overflow: 'hidden', flexShrink: 0,
}}>
<div style={{
width: `${(sourceProgress.step / sourceProgress.total) * 100}%`, height: '100%',
background: sourceProgress.status === 'error' ? '#fca5a5' : '#a7f3d0',
borderRadius: 3, transition: 'width 0.5s ease',
}} />
</div>
)}
{/* 更新结果提示 */}
{updateResult && (
<span style={{
fontSize: 12, fontWeight: 600,
color: updateResult.success ? '#a7f3d0' : '#fca5a5',
}}>
{updateResult.message}
{updateResult.success && !updateResult.message.includes(t('update.alreadyLatest')) && (
<button
onClick={() => window.location.reload()}
style={{
marginLeft: 8, background: 'rgba(255,255,255,0.25)',
border: '1px solid rgba(255,255,255,0.4)', borderRadius: 6,
padding: '2px 10px', cursor: 'pointer', color: 'inherit',
fontWeight: 700, fontSize: 12,
}}
>
{t('update.refreshNow')}
</button>
)}
</span>
)}
{/* 不支持自动更新时:显示下载链接 */}
{!canAutoUpdate && (
<>
<a
href="https://github.com/YuanShiJiLoong/author"
target="_blank"
rel="noopener noreferrer"
className="update-banner-link"
>
{t('update.viewSource')}
</a>
<a
href="https://github.com/YuanShiJiLoong/author/releases/latest"
target="_blank"
rel="noopener noreferrer"
className="update-banner-link"
>
{t('update.downloadClient')}
</a>
</>
)}
<button
className="update-banner-dismiss"
onClick={handleDismiss}
title={t('update.dismiss')}
>
✕
</button>
</div>
{/* 更新日志(源码部署) */}
{updateResult?.logs && updateResult.logs.length > 0 && (
<div style={{
background: 'rgba(0,0,0,0.3)', padding: '8px 20px',
fontSize: 11, fontFamily: 'var(--font-mono, monospace)',
color: 'rgba(255,255,255,0.85)', maxHeight: 120, overflowY: 'auto',
lineHeight: 1.6,
}}>
{updateResult.logs.map((l, i) => (
<div key={i}>{l.msg}</div>
))}
</div>
)}
</div>
);
}