summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-14 13:35:06 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-14 13:35:06 +0800
commitb21df2f9abb9c8d5da0b77f2d3756802f95a1ad2 (patch)
tree72ae1c175c2085988cdddd15b1e181c0f69ad516
parentd0023c586cfe90a928f70c0d81e388b495d5ae28 (diff)
downloadDropOut-b21df2f9abb9c8d5da0b77f2d3756802f95a1ad2.tar.gz
DropOut-b21df2f9abb9c8d5da0b77f2d3756802f95a1ad2.zip
feat: display download rate and progress with concurrency support
-rw-r--r--src-tauri/src/core/downloader.rs33
-rw-r--r--ui/src/lib/DownloadMonitor.svelte98
2 files changed, 124 insertions, 7 deletions
diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs
index 5f6ec80..5a0605b 100644
--- a/src-tauri/src/core/downloader.rs
+++ b/src-tauri/src/core/downloader.rs
@@ -1,6 +1,7 @@
use futures::StreamExt;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
+use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use tauri::{Emitter, Window};
use tokio::io::AsyncWriteExt;
@@ -19,11 +20,20 @@ pub struct ProgressEvent {
pub downloaded: u64,
pub total: u64,
pub status: String, // "Downloading", "Verifying", "Finished", "Error"
+ pub completed_files: usize,
+ pub total_files: usize,
+ pub total_downloaded_bytes: u64,
}
-pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<(), String> {
- let client = reqwest::Client::new();
- let semaphore = Arc::new(Semaphore::new(10)); // Max 10 concurrent downloads
+pub async fn download_files(window: Window, tasks: Vec<DownloadTask>, max_concurrent: usize) -> Result<(), String> {
+ let client = reqwest::Client::builder()
+ .pool_max_idle_per_host(max_concurrent)
+ .build()
+ .map_err(|e| e.to_string())?;
+ let semaphore = Arc::new(Semaphore::new(max_concurrent));
+ let completed_files = Arc::new(AtomicUsize::new(0));
+ let total_downloaded_bytes = Arc::new(AtomicU64::new(0));
+ let total_files = tasks.len();
// Notify start (total files)
let _ = window.emit("download-start", tasks.len());
@@ -32,6 +42,8 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
let client = client.clone();
let window = window.clone();
let semaphore = semaphore.clone();
+ let completed_files = completed_files.clone();
+ let total_downloaded_bytes = total_downloaded_bytes.clone();
async move {
let _permit = semaphore.acquire().await.unwrap();
@@ -46,6 +58,9 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
downloaded: 0,
total: 0,
status: "Verifying".into(),
+ completed_files: completed_files.load(Ordering::Relaxed),
+ total_files,
+ total_downloaded_bytes: total_downloaded_bytes.load(Ordering::Relaxed),
},
);
@@ -57,6 +72,7 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
let result = hex::encode(hasher.finalize());
if &result == expected_sha1 {
// Already valid
+ let completed = completed_files.fetch_add(1, Ordering::Relaxed) + 1;
let _ = window.emit(
"download-progress",
ProgressEvent {
@@ -64,6 +80,9 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
downloaded: 0,
total: 0,
status: "Skipped".into(),
+ completed_files: completed,
+ total_files,
+ total_downloaded_bytes: total_downloaded_bytes.load(Ordering::Relaxed),
},
);
return Ok(());
@@ -93,6 +112,7 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
return Err(format!("Write error: {}", e));
}
downloaded += chunk.len() as u64;
+ let total_bytes = total_downloaded_bytes.fetch_add(chunk.len() as u64, Ordering::Relaxed) + chunk.len() as u64;
let _ = window.emit(
"download-progress",
ProgressEvent {
@@ -100,6 +120,9 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
downloaded,
total: total_size,
status: "Downloading".into(),
+ completed_files: completed_files.load(Ordering::Relaxed),
+ total_files,
+ total_downloaded_bytes: total_bytes,
},
);
}
@@ -111,6 +134,7 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
Err(e) => return Err(format!("Request error: {}", e)),
}
+ let completed = completed_files.fetch_add(1, Ordering::Relaxed) + 1;
let _ = window.emit(
"download-progress",
ProgressEvent {
@@ -118,6 +142,9 @@ pub async fn download_files(window: Window, tasks: Vec<DownloadTask>) -> Result<
downloaded: 0,
total: 0,
status: "Finished".into(),
+ completed_files: completed,
+ total_files,
+ total_downloaded_bytes: total_downloaded_bytes.load(Ordering::Relaxed),
},
);
diff --git a/ui/src/lib/DownloadMonitor.svelte b/ui/src/lib/DownloadMonitor.svelte
index b796591..e4920ad 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,45 @@
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) {
+ totalProgress = (completedFiles / 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 based on files remaining
+ if (downloadSpeed > 0 && completedFiles < totalFiles) {
+ // Rough estimate: assume average file size based on downloaded data
+ const avgBytesPerFile = completedFiles > 0 ? totalDownloadedBytes / completedFiles : totalDownloadedBytes;
+ const remainingFiles = totalFiles - completedFiles;
+ const estimatedRemainingBytes = avgBytesPerFile * remainingFiles;
+ etaSeconds = estimatedRemainingBytes / downloadSpeed;
+ } else {
+ etaSeconds = 0;
+ }
}
);
unlistenComplete = await listen("download-complete", () => {
statusText = "Done!";
progress = 100;
+ totalProgress = 100;
setTimeout(() => {
visible = false;
}, 2000);
@@ -71,6 +125,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 +154,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"