8acb6dc246
Render the active peer count already carried by download progress events in the large download progress control. The peer chip appears between speed and ETA, uses singular/plural copy, and hides after the ETA when the detail modal gets very narrow. This keeps the UI aligned with the design reference without changing backend state ownership or download progress plumbing. Test Plan: - git diff --check - git diff --cached --check - just frontend-test - just build
140 lines
5.0 KiB
TypeScript
140 lines
5.0 KiB
TypeScript
import { CSSProperties, MouseEvent } from 'react';
|
|
|
|
import { Game } from '../lib/types';
|
|
import {
|
|
downloadProgressPercent,
|
|
formatDownloadBytes,
|
|
formatDownloadEta,
|
|
formatDownloadSpeed,
|
|
formatDownloadSpeedShort,
|
|
} from '../lib/gameState';
|
|
|
|
import { Icon } from './Icon';
|
|
|
|
interface Props {
|
|
game: Game;
|
|
size?: 'md' | 'lg';
|
|
full?: boolean;
|
|
onCancel?: (game: Game) => void;
|
|
}
|
|
|
|
const progressStats = (game: Game) => {
|
|
const progress = game.download_progress;
|
|
const downloaded = progress?.downloaded_bytes ?? 0;
|
|
const total = progress?.total_bytes ?? game.size;
|
|
const speed = progress?.bytes_per_second ?? 0;
|
|
const remaining = Math.max(0, total - downloaded);
|
|
const etaSeconds = speed > 0 ? remaining / speed : Number.POSITIVE_INFINITY;
|
|
|
|
return {
|
|
pct: Math.min(99, Math.round(downloadProgressPercent(game))),
|
|
downloaded,
|
|
total,
|
|
speed,
|
|
eta: etaSeconds,
|
|
activePeerCount: progress?.active_peer_count ?? 0,
|
|
};
|
|
};
|
|
|
|
export const DownloadProgress = ({ game, size = 'md', full = false, onCancel }: Props) => {
|
|
const stats = progressStats(game);
|
|
const progressStyle = {
|
|
'--download-progress': `${stats.pct}%`,
|
|
} as CSSProperties;
|
|
const className = [
|
|
'dl',
|
|
size === 'lg' ? 'dl-lg' : 'dl-md',
|
|
full ? 'dl-full' : '',
|
|
].filter(Boolean).join(' ');
|
|
|
|
const handleCancel = (event: MouseEvent<HTMLButtonElement>) => {
|
|
event.stopPropagation();
|
|
onCancel?.(game);
|
|
};
|
|
|
|
if (size === 'lg') {
|
|
const peerUnit = stats.activePeerCount === 1 ? 'peer' : 'peers';
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
role="progressbar"
|
|
aria-label={`Downloading ${game.name}`}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-valuenow={stats.pct}
|
|
style={progressStyle}
|
|
>
|
|
<div className="dl-fill" aria-hidden />
|
|
<div className="dl-lg-grid">
|
|
<div className="dl-lg-primary">
|
|
<span className="dl-pulse" aria-hidden />
|
|
<span className="dl-label">Downloading</span>
|
|
</div>
|
|
<div className="dl-lg-secondary">
|
|
<span className="dl-bytes">
|
|
<strong>{formatDownloadBytes(stats.downloaded)}</strong>
|
|
<span className="dl-of"> / {formatDownloadBytes(stats.total)}</span>
|
|
</span>
|
|
<span className="dl-sep">·</span>
|
|
<span className="dl-speed">{formatDownloadSpeed(stats.speed)}</span>
|
|
{stats.activePeerCount > 0 && (
|
|
<>
|
|
<span className="dl-sep dl-sep-peers">·</span>
|
|
<span
|
|
className="dl-peers"
|
|
title={`Downloading from ${stats.activePeerCount} ${peerUnit} on the LAN`}
|
|
>
|
|
<Icon.users />
|
|
<span>
|
|
from <strong>{stats.activePeerCount}</strong> {peerUnit}
|
|
</span>
|
|
</span>
|
|
</>
|
|
)}
|
|
<span className="dl-sep dl-sep-eta">·</span>
|
|
<span className="dl-eta">{formatDownloadEta(stats.eta)} left</span>
|
|
</div>
|
|
<div className="dl-lg-pct">
|
|
{stats.pct}
|
|
<span className="dl-pct-sym">%</span>
|
|
</div>
|
|
{onCancel && (
|
|
<button
|
|
type="button"
|
|
className="dl-cancel"
|
|
onClick={handleCancel}
|
|
aria-label={`Cancel download of ${game.name}`}
|
|
>
|
|
<Icon.close />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={className}
|
|
role="progressbar"
|
|
aria-label={`Downloading ${game.name}`}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-valuenow={stats.pct}
|
|
title={`${stats.pct}% · ${formatDownloadSpeed(stats.speed)} · ${formatDownloadEta(stats.eta)} left`}
|
|
style={progressStyle}
|
|
>
|
|
<div className="dl-fill" aria-hidden />
|
|
<div className="dl-md-row">
|
|
<span className="dl-pct">
|
|
<span className="dl-pulse" aria-hidden />
|
|
{stats.pct}
|
|
<span className="dl-pct-sym">%</span>
|
|
</span>
|
|
<span className="dl-speed">{formatDownloadSpeedShort(stats.speed)}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|