Separate game file downloads and verification with progress timing

This commit is contained in:
2026-10-04 12:51:05 -04:00
parent 68323c0baa
commit f8c60fd101
17 changed files with 1150 additions and 132 deletions
@@ -0,0 +1,52 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { HostLibraryCache } from '$lib/game-files.js';
import { formatGameBytes } from '$lib/game-files.js';
import { formatDuration } from '$ui/duration.js';
import { gameFileTransferProgress } from '$ui/game-file-transfer.js';
let { cache, connected }: { cache: HostLibraryCache; connected: boolean } = $props();
let now = $state(Date.now());
const progress = $derived(gameFileTransferProgress(cache, connected, now));
const downloading = $derived(progress.phase === 'downloading' && progress.active);
const verifying = $derived(progress.phase === 'verifying' && progress.active);
const downloaded = $derived(progress.complete || progress.phase === 'verifying');
onMount(() => {
const timer = setInterval(() => {
if (cache.transfer && connected && (cache.status === 'downloading' || cache.status === 'verifying')) now = Date.now();
}, 1000);
return () => clearInterval(timer);
});
</script>
<div class="transfer-progress">
<ol aria-label="Game file preparation steps">
<li class:active={downloading} class:complete={downloaded}>1. {downloaded ? 'Downloaded' : downloading ? 'Downloading' : 'Download'}</li>
<li class:active={verifying} class:complete={progress.complete}>2. {progress.complete ? 'Verified' : verifying ? 'Verifying' : 'Verify'}</li>
</ol>
{#if progress.phase && !progress.complete}
<progress max={progress.total || 1} value={progress.total ? progress.phaseBytes ?? undefined : undefined} aria-label={progress.phase === 'verifying' ? 'Host verification progress' : 'Host download progress'}></progress>
<span class="bytes">{#if progress.phaseBytes !== null}{formatGameBytes(progress.phaseBytes)} / {formatGameBytes(progress.total)} {progress.phase === 'verifying' ? 'checked' : 'downloaded'}{:else}Checking every file…{/if}</span>
{/if}
{#if progress.elapsedSeconds !== null}
<div class="timing" aria-live="off">
<span>Elapsed {formatDuration(progress.elapsedSeconds * 1000)}</span>
{#if progress.bytesPerSecond !== null}<span>{formatGameBytes(progress.bytesPerSecond)}/s</span>{/if}
{#if progress.etaSeconds !== null}<span>About {formatDuration(Math.ceil(progress.etaSeconds) * 1000)} {progress.phase === 'verifying' ? 'verification' : 'download'} remaining</span>
{:else if progress.live && progress.phaseBytes !== progress.total}<span>Estimating {progress.phase === 'verifying' ? 'verification' : 'download'} time remaining…</span>{/if}
</div>
{/if}
{#if progress.active && !connected}<span class="muted">Host offline · waiting to resume updates</span>
{:else if progress.stale}<span class="muted">Waiting for a fresh host report</span>
{:else if downloading}<span class="muted">All files download before verification starts.</span>{/if}
</div>
<style>
.transfer-progress { display:grid; gap:6px; min-width:200px; }
ol { display:flex; flex-wrap:wrap; gap:6px 16px; list-style:none; margin:0; padding:0; font-size:var(--text-xs); color:var(--text-secondary); }
li.active { color:var(--text-primary); font-weight:600; } li.complete { color:var(--accent-primary-hover); }
progress { display:block; width:100%; margin:0; accent-color:var(--accent-primary); }
.bytes,.timing,.muted { font-size:var(--text-xs); color:var(--text-secondary); font-variant-numeric:tabular-nums; line-height:1.5; }
.timing { display:flex; flex-wrap:wrap; gap:2px 12px; }
</style>
+5 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import type { Host } from '$lib/hosts.js';
import { formatGameBytes } from '$lib/game-files.js';
import GameFileTransferProgress from '$ui/components/GameFileTransferProgress.svelte';
let { host }: { host: Host } = $props();
</script>
@@ -13,7 +14,9 @@
<li>
<div class="row"><strong>{item.label}</strong><span>{item.state === 'blocked' ? 'Needs attention' : item.state}</span></div>
<p class:blocked={item.state === 'blocked'}>{item.detail}</p>
{#if item.bytesTotal && item.bytesDownloaded !== undefined && item.state === 'preparing'}
{#if item.cache && item.cache.status !== 'missing'}
<div class="transfer"><GameFileTransferProgress cache={item.cache} connected={host.connected} /></div>
{:else if item.bytesTotal && item.bytesDownloaded !== undefined && item.state === 'preparing'}
<progress max={item.bytesTotal} value={item.bytesDownloaded} aria-label={`${item.label} download progress`}></progress>
<small>{formatGameBytes(item.bytesDownloaded)} / {formatGameBytes(item.bytesTotal)}</small>
{/if}
@@ -28,4 +31,5 @@
.preparation { display:grid; gap:12px; } p { margin:0; color:var(--text-secondary); font-size:var(--text-sm); line-height:1.6; } .status { color:var(--text-primary); }
ul { list-style:none; padding:0; margin:0; display:grid; gap:16px; } li { min-width:0; } .row { display:flex; justify-content:space-between; gap:12px; font-size:var(--text-sm); } strong { overflow-wrap:anywhere; } span { text-transform:capitalize; color:var(--text-secondary); }
li p { margin-top:4px; } .blocked { color:var(--state-danger); } progress { width:100%; margin-top:8px; accent-color:var(--accent-primary); } small { display:block; color:var(--text-secondary); font-size:var(--text-xs); } a { color:var(--accent-primary-hover); }
.transfer { margin-top:8px; }
</style>