12a0d7abe9
Mirror the design-doc update in the actual download progress component so the GUI matches the trimmed chip. Previously the large progress panel rendered `[icon] from N peer(s)` inline; now it renders just `[icon] N`, with the full "Downloading from N peers on the LAN" sentence retained as the `title` tooltip for discoverability. Changes: - `DownloadProgress.tsx` (lg variant): drop the "from" / unit text from the inline span, keeping only the count. `peerUnit` stays in scope because the tooltip still needs singular/plural. - `launcher.css`: collapse `.dl-peers` and `.dl-peers strong` into a single rule that puts the t-1 colour, 600 weight and tabular-nums directly on the chip (the inner `<strong>` no longer exists). Gap drops from 5px to 4px to match the tighter icon+number layout. - Container queries: peers drops at <=240px and ETA drops at <=320px, matching the new thresholds in the design reference. The narrower chip simply fits in smaller modals, so the old 300/380 cutoffs were hiding stats that would have rendered fine. Test Plan - `just frontend-test` (passes) - `just run`, start a download, confirm the chip reads `[icon] N`, hover shows the tooltip, and narrowing the window collapses ETA before peers at the new breakpoints.
138 lines
4.9 KiB
TypeScript
138 lines
4.9 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>{stats.activePeerCount}</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>
|
|
);
|
|
};
|