diff options
| author | 2026-01-14 14:24:25 +0800 | |
|---|---|---|
| committer | 2026-01-14 14:24:25 +0800 | |
| commit | 80bd692e633002e1920c9ddae7cabc7ec2eadb6a (patch) | |
| tree | 36d59d2fcd87d118c6db443c9208bf34b1025eff /ui/src | |
| parent | e8e139c07d05e2f29f04906019dff5f3c520f8cc (diff) | |
| parent | 561469b5a895d7c99fe6c9e73266b49ebe4237b8 (diff) | |
| download | DropOut-80bd692e633002e1920c9ddae7cabc7ec2eadb6a.tar.gz DropOut-80bd692e633002e1920c9ddae7cabc7ec2eadb6a.zip | |
Merge pull request #20 from HsiangNianian/dev
Diffstat (limited to 'ui/src')
| -rw-r--r-- | ui/src/components/SettingsView.svelte | 24 | ||||
| -rw-r--r-- | ui/src/components/StatusToast.svelte | 42 | ||||
| -rw-r--r-- | ui/src/components/VersionsView.svelte | 23 | ||||
| -rw-r--r-- | ui/src/lib/DownloadMonitor.svelte | 107 | ||||
| -rw-r--r-- | ui/src/stores/settings.svelte.ts | 1 | ||||
| -rw-r--r-- | ui/src/types/index.ts | 1 |
6 files changed, 175 insertions, 23 deletions
diff --git a/ui/src/components/SettingsView.svelte b/ui/src/components/SettingsView.svelte index 9f260c1..801970b 100644 --- a/ui/src/components/SettingsView.svelte +++ b/ui/src/components/SettingsView.svelte @@ -117,6 +117,30 @@ </div> </div> + <!-- Download Settings --> + <div class="bg-zinc-800/50 p-6 rounded-lg border border-zinc-700"> + <h3 + class="block text-sm font-bold text-zinc-400 mb-4 uppercase tracking-wide" + >Download Settings</h3> + <div> + <label for="download-threads" class="block text-xs text-zinc-500 mb-1" + >Concurrent Download Threads</label + > + <input + id="download-threads" + bind:value={settingsState.settings.download_threads} + type="number" + min="1" + max="128" + step="1" + class="bg-zinc-950 text-white w-full p-3 rounded border border-zinc-700 focus:border-indigo-500 outline-none" + /> + <p class="text-xs text-zinc-500 mt-2"> + Number of concurrent download threads (1-128). Higher values increase download speed but use more bandwidth and system resources. Default: 32 + </p> + </div> + </div> + <div class="pt-4"> <button onclick={() => settingsState.saveSettings()} diff --git a/ui/src/components/StatusToast.svelte b/ui/src/components/StatusToast.svelte index b1feffc..0d68778 100644 --- a/ui/src/components/StatusToast.svelte +++ b/ui/src/components/StatusToast.svelte @@ -3,28 +3,34 @@ </script> {#if uiState.status !== "Ready"} - <div - class="absolute top-12 right-12 bg-zinc-800/90 backdrop-blur border border-zinc-600 p-4 rounded-lg shadow-2xl max-w-sm animate-in fade-in slide-in-from-top-4 duration-300 z-50 group" - > - <div class="flex justify-between items-start mb-1"> - <div class="text-xs text-zinc-400 uppercase font-bold">Status</div> - <button - onclick={() => uiState.setStatus("Ready")} - class="text-zinc-500 hover:text-white transition -mt-1 -mr-1 p-1" - > - ✕ - </button> + {#key uiState.status} + <div + class="absolute top-12 right-12 bg-zinc-800/90 backdrop-blur border border-zinc-600 p-4 rounded-lg shadow-2xl max-w-sm animate-in fade-in slide-in-from-top-4 duration-300 z-50 group" + > + <div class="flex justify-between items-start mb-1"> + <div class="text-xs text-zinc-400 uppercase font-bold">Status</div> + <button + onclick={() => uiState.setStatus("Ready")} + class="text-zinc-500 hover:text-white transition -mt-1 -mr-1 p-1" + > + ✕ + </button> + </div> + <div class="font-mono text-sm whitespace-pre-wrap mb-2">{uiState.status}</div> + <div class="w-full bg-zinc-700/50 h-1 rounded-full overflow-hidden"> + <div + class="h-full bg-indigo-500 origin-left w-full progress-bar" + ></div> + </div> </div> - <div class="font-mono text-sm whitespace-pre-wrap mb-2">{uiState.status}</div> - <div class="w-full bg-zinc-700/50 h-1 rounded-full overflow-hidden"> - <div - class="h-full bg-indigo-500 animate-[progress_5s_linear_forwards] origin-left w-full" - ></div> - </div> - </div> + {/key} {/if} <style> + .progress-bar { + animation: progress 5s linear forwards; + } + @keyframes progress { from { transform: scaleX(1); diff --git a/ui/src/components/VersionsView.svelte b/ui/src/components/VersionsView.svelte index 8c0ddfe..98261b8 100644 --- a/ui/src/components/VersionsView.svelte +++ b/ui/src/components/VersionsView.svelte @@ -1,14 +1,35 @@ <script lang="ts"> import { gameState } from "../stores/game.svelte"; + + let searchQuery = $state(""); + let normalizedQuery = $derived( + searchQuery.trim().toLowerCase().replace(/。/g, ".") + ); + + let filteredVersions = $derived( + gameState.versions.filter((v) => + v.id.toLowerCase().includes(normalizedQuery) + ) + ); </script> <div class="p-8 h-full overflow-y-auto bg-zinc-900"> <h2 class="text-3xl font-bold mb-6">Versions</h2> + + <input + type="text" + placeholder="Search versions..." + class="w-full p-3 mb-4 bg-zinc-800 border border-zinc-700 rounded text-white focus:outline-none focus:border-green-500 transition-colors" + bind:value={searchQuery} + /> + <div class="grid gap-2"> {#if gameState.versions.length === 0} <div class="text-zinc-500">Loading versions...</div> + {:else if filteredVersions.length === 0 && normalizedQuery.length > 0} + <div class="text-zinc-500">No versions found matching "{searchQuery}"</div> {:else} - {#each gameState.versions as version} + {#each filteredVersions as version} <button class="flex items-center justify-between p-4 bg-zinc-800 rounded hover:bg-zinc-700 transition text-left border border-zinc-700 {gameState.selectedVersion === version.id diff --git a/ui/src/lib/DownloadMonitor.svelte b/ui/src/lib/DownloadMonitor.svelte index b796591..52c935c 100644 --- a/ui/src/lib/DownloadMonitor.svelte +++ b/ui/src/lib/DownloadMonitor.svelte @@ -9,11 +9,16 @@ downloaded: number; // in bytes total: number; // in bytes status: string; + completed_files: number; + total_files: number; + total_downloaded_bytes: number; } let currentFile = ""; - let progress = 0; // percentage 0-100 + let progress = 0; // percentage 0-100 (current file) + let totalProgress = 0; // percentage 0-100 (all files) let totalFiles = 0; + let completedFiles = 0; let statusText = "Preparing..."; let unlistenProgress: () => void; let unlistenStart: () => void; @@ -21,13 +26,30 @@ let downloadedBytes = 0; let totalBytes = 0; + // Speed and ETA tracking + let downloadSpeed = 0; // bytes per second + let etaSeconds = 0; + let startTime = 0; + let totalDownloadedBytes = 0; + let lastUpdateTime = 0; + let lastTotalBytes = 0; + onMount(async () => { unlistenStart = await listen<number>("download-start", (event) => { visible = true; totalFiles = event.payload; + completedFiles = 0; progress = 0; + totalProgress = 0; statusText = "Starting download..."; currentFile = ""; + // Reset speed tracking + startTime = Date.now(); + totalDownloadedBytes = 0; + downloadSpeed = 0; + etaSeconds = 0; + lastUpdateTime = Date.now(); + lastTotalBytes = 0; }); unlistenProgress = await listen<DownloadEvent>( @@ -36,8 +58,7 @@ const payload = event.payload; currentFile = payload.file; - // Simple file progress for now. Global progress would require tracking all files. - // For single file (Client jar), this is accurate. + // Current file progress downloadedBytes = payload.downloaded; totalBytes = payload.total; @@ -46,12 +67,54 @@ if (payload.total > 0) { progress = (payload.downloaded / payload.total) * 100; } + + // Total progress (all files) + completedFiles = payload.completed_files; + totalFiles = payload.total_files; + if (totalFiles > 0) { + const currentFileFraction = + payload.total > 0 ? payload.downloaded / payload.total : 0; + totalProgress = ((completedFiles + currentFileFraction) / totalFiles) * 100; + } + + // Calculate download speed (using moving average) + totalDownloadedBytes = payload.total_downloaded_bytes; + const now = Date.now(); + const timeDiff = (now - lastUpdateTime) / 1000; // seconds + + if (timeDiff >= 0.5) { // Update speed every 0.5 seconds + const bytesDiff = totalDownloadedBytes - lastTotalBytes; + const instantSpeed = bytesDiff / timeDiff; + // Smooth the speed with exponential moving average + downloadSpeed = downloadSpeed === 0 ? instantSpeed : downloadSpeed * 0.7 + instantSpeed * 0.3; + lastUpdateTime = now; + lastTotalBytes = totalDownloadedBytes; + } + + // Estimate remaining time + if (downloadSpeed > 0 && completedFiles < totalFiles) { + const remainingFiles = totalFiles - completedFiles; + let estimatedRemainingBytes: number; + + if (completedFiles > 0) { + // Use average size of completed files to estimate remaining files + const avgBytesPerCompletedFile = totalDownloadedBytes / completedFiles; + estimatedRemainingBytes = avgBytesPerCompletedFile * remainingFiles; + } else { + // No completed files yet: estimate based only on current file's remaining bytes + estimatedRemainingBytes = Math.max(totalBytes - downloadedBytes, 0); + } + etaSeconds = estimatedRemainingBytes / downloadSpeed; + } else { + etaSeconds = 0; + } } ); unlistenComplete = await listen("download-complete", () => { statusText = "Done!"; progress = 100; + totalProgress = 100; setTimeout(() => { visible = false; }, 2000); @@ -71,6 +134,24 @@ const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } + + function formatSpeed(bytesPerSecond: number) { + if (bytesPerSecond === 0) return "-- /s"; + return formatBytes(bytesPerSecond) + "/s"; + } + + function formatTime(seconds: number) { + if (seconds <= 0 || !isFinite(seconds)) return "--"; + if (seconds < 60) return `${Math.round(seconds)}s`; + if (seconds < 3600) { + const mins = Math.floor(seconds / 60); + const secs = Math.round(seconds % 60); + return `${mins}m ${secs}s`; + } + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return `${hours}h ${mins}m`; + } </script> {#if visible} @@ -82,11 +163,29 @@ <span class="text-xs text-zinc-400">{statusText}</span> </div> + <!-- Total Progress Bar --> + <div class="mb-3"> + <div class="flex justify-between text-[10px] text-zinc-400 mb-1"> + <span>Total Progress</span> + <span>{completedFiles} / {totalFiles} files</span> + </div> + <div class="w-full bg-zinc-800 rounded-full h-2.5 overflow-hidden"> + <div + class="bg-gradient-to-r from-blue-500 to-cyan-400 h-2.5 rounded-full transition-all duration-200" + style="width: {totalProgress}%" + ></div> + </div> + <div class="flex justify-between text-[10px] text-zinc-500 font-mono mt-0.5"> + <span>{formatSpeed(downloadSpeed)} · ETA: {formatTime(etaSeconds)}</span> + <span>{completedFiles < totalFiles ? Math.floor(totalProgress) : 100}%</span> + </div> + </div> + <div class="text-xs text-zinc-300 truncate mb-1" title={currentFile}> {currentFile || "Waiting..."} </div> - <!-- Progress Bar --> + <!-- Current File Progress Bar --> <div class="w-full bg-zinc-800 rounded-full h-2 mb-2 overflow-hidden"> <div class="bg-gradient-to-r from-green-500 to-emerald-400 h-2 rounded-full transition-all duration-200" diff --git a/ui/src/stores/settings.svelte.ts b/ui/src/stores/settings.svelte.ts index 989172c..397b9a6 100644 --- a/ui/src/stores/settings.svelte.ts +++ b/ui/src/stores/settings.svelte.ts @@ -9,6 +9,7 @@ export class SettingsState { java_path: "java", width: 854, height: 480, + download_threads: 32, }); javaInstallations = $state<JavaInstallation[]>([]); isDetectingJava = $state(false); diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index b7ff0a0..1f83585 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -29,6 +29,7 @@ export interface LauncherConfig { java_path: string; width: number; height: number; + download_threads: number; } export interface JavaInstallation { |