9a0f3f554b
Check server version on launch, download/install/relaunch if newer, then show the main UI. Manual update check no longer triggers full install. Co-authored-by: Cursor <cursoragent@cursor.com>
160 lines
5.7 KiB
TypeScript
160 lines
5.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import {
|
|
runStartupAutoUpdate,
|
|
type DesktopUpdatePhase,
|
|
type DesktopUpdateProgress,
|
|
} from './bridge/updater';
|
|
import '@frontend/App.css';
|
|
|
|
const PHASE_LABEL: Record<DesktopUpdatePhase, string> = {
|
|
checking: '업데이트 확인 중',
|
|
uptodate: '최신 버전',
|
|
available: '새 버전 발견',
|
|
downloading: '다운로드 중',
|
|
installing: '설치 중',
|
|
relaunching: '재시작 중',
|
|
error: '업데이트 실패',
|
|
};
|
|
|
|
function formatBytes(n?: number): string {
|
|
if (n == null || !Number.isFinite(n) || n <= 0) return '';
|
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
}
|
|
|
|
function StartupUpdateSplash({ progress }: { progress: DesktopUpdateProgress | null }) {
|
|
const phase = progress?.phase ?? 'checking';
|
|
const isActive = phase === 'checking' || phase === 'downloading' || phase === 'installing' || phase === 'relaunching';
|
|
const showBar = phase === 'available' || phase === 'downloading' || phase === 'installing';
|
|
const barPct = phase === 'installing' ? 100 : progress?.downloadPercent;
|
|
|
|
const panelClass = phase === 'error'
|
|
? 'desktop-update-progress--failed'
|
|
: isActive
|
|
? 'desktop-update-progress--active'
|
|
: '';
|
|
|
|
return (
|
|
<div className="desktop-startup-update">
|
|
<div className="desktop-startup-update-card">
|
|
<h1 className="desktop-startup-update-title">GoldenChart</h1>
|
|
<p className="desktop-startup-update-sub">
|
|
{progress?.currentVersion ? `현재 v${progress.currentVersion}` : '시작 중…'}
|
|
{progress?.newVersion && progress.newVersion !== progress.currentVersion && (
|
|
<> → v{progress.newVersion}</>
|
|
)}
|
|
</p>
|
|
|
|
<div className={`desktop-update-progress ${panelClass}`.trim()}>
|
|
<div className="desktop-update-progress-head">
|
|
<span className="desktop-update-progress-title">
|
|
{isActive && <span className="desktop-update-spinner" aria-hidden />}
|
|
{PHASE_LABEL[phase]}
|
|
</span>
|
|
{phase === 'downloading' && progress?.downloadPercent != null && (
|
|
<span className="desktop-update-pct">{progress.downloadPercent}%</span>
|
|
)}
|
|
</div>
|
|
|
|
{showBar && (
|
|
<div
|
|
className="desktop-update-bar"
|
|
role="progressbar"
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-valuenow={barPct ?? undefined}
|
|
>
|
|
{barPct != null ? (
|
|
<div
|
|
className="desktop-update-bar-fill desktop-update-bar-fill--determinate"
|
|
style={{ width: `${barPct}%` }}
|
|
/>
|
|
) : (
|
|
<div className="desktop-update-bar-fill desktop-update-bar-fill--indeterminate" />
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<ul className="desktop-update-steps">
|
|
<li className={`desktop-update-step${phase === 'checking' ? ' active' : phase !== 'error' ? ' done' : ''}`}>
|
|
<span className="desktop-update-step-dot" aria-hidden />
|
|
서버에서 버전 확인
|
|
</li>
|
|
<li className={`desktop-update-step${
|
|
phase === 'available' || phase === 'downloading' ? ' active'
|
|
: ['installing', 'relaunching', 'uptodate'].includes(phase) ? ' done' : ''
|
|
}`}>
|
|
<span className="desktop-update-step-dot" aria-hidden />
|
|
업데이트 다운로드
|
|
{progress?.downloadedBytes && progress.totalBytes ? (
|
|
<span className="desktop-update-bytes">
|
|
{formatBytes(progress.downloadedBytes)} / {formatBytes(progress.totalBytes)}
|
|
</span>
|
|
) : null}
|
|
</li>
|
|
<li className={`desktop-update-step${
|
|
phase === 'installing' ? ' active' : phase === 'relaunching' || phase === 'uptodate' ? ' done' : ''
|
|
}`}>
|
|
<span className="desktop-update-step-dot" aria-hidden />
|
|
설치 및 재시작
|
|
</li>
|
|
</ul>
|
|
|
|
{progress?.message && (
|
|
<p className={`desktop-update-message${phase === 'error' ? ' desktop-update-message--error' : ''}`}>
|
|
{progress.message}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** StrictMode 이중 실행 방지 — 시작 업데이트는 한 번만 */
|
|
let startupUpdateTask: ReturnType<typeof runStartupAutoUpdate> | null = null;
|
|
const startupProgressListeners = new Set<(p: DesktopUpdateProgress) => void>();
|
|
|
|
function emitStartupProgress(p: DesktopUpdateProgress) {
|
|
for (const listener of startupProgressListeners) listener(p);
|
|
}
|
|
|
|
function getStartupUpdateTask() {
|
|
if (!startupUpdateTask) {
|
|
startupUpdateTask = runStartupAutoUpdate(emitStartupProgress);
|
|
}
|
|
return startupUpdateTask;
|
|
}
|
|
|
|
/** 시작 시 자동 업데이트 후 children(메인 앱) 렌더 */
|
|
export function StartupUpdateGate({ children }: { children: React.ReactNode }) {
|
|
const [ready, setReady] = useState(false);
|
|
const [progress, setProgress] = useState<DesktopUpdateProgress | null>({
|
|
phase: 'checking',
|
|
message: '서버에서 새 버전을 확인하는 중…',
|
|
});
|
|
|
|
useEffect(() => {
|
|
const listener = (p: DesktopUpdateProgress) => setProgress(p);
|
|
startupProgressListeners.add(listener);
|
|
|
|
void getStartupUpdateTask().then(({ shouldContinue, result }) => {
|
|
if (!shouldContinue) return;
|
|
if (result.phase === 'error') {
|
|
console.warn('[startup-update]', result.message);
|
|
}
|
|
setReady(true);
|
|
});
|
|
|
|
return () => {
|
|
startupProgressListeners.delete(listener);
|
|
};
|
|
}, []);
|
|
|
|
if (!ready) {
|
|
return <StartupUpdateSplash progress={progress} />;
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|