From b21df2f9abb9c8d5da0b77f2d3756802f95a1ad2 Mon Sep 17 00:00:00 2001
From: HsiangNianian <i@jyunko.cn>
Date: Wed, 14 Jan 2026 13:35:06 +0800
Subject: feat: display download rate and progress with concurrency support

---
 ui/src/lib/DownloadMonitor.svelte | 98 +++++++++++++++++++++++++++++++++++++--
 1 file changed, 94 insertions(+), 4 deletions(-)

(limited to 'ui/src/lib/DownloadMonitor.svelte')

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"
-- 
cgit v1.2.3-70-g09d2


From d559a4624efb3cf5688a3df03a77f90999c42233 Mon Sep 17 00:00:00 2001
From: 简律纯 <i@jyunko.cn>
Date: Wed, 14 Jan 2026 13:50:25 +0800
Subject: feat: Consider incorporating totalDownloadedBytes into the
 calculation for a smoother progress indicator

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
 ui/src/lib/DownloadMonitor.svelte | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

(limited to 'ui/src/lib/DownloadMonitor.svelte')

diff --git a/ui/src/lib/DownloadMonitor.svelte b/ui/src/lib/DownloadMonitor.svelte
index e4920ad..5dc8269 100644
--- a/ui/src/lib/DownloadMonitor.svelte
+++ b/ui/src/lib/DownloadMonitor.svelte
@@ -72,7 +72,9 @@
         completedFiles = payload.completed_files;
         totalFiles = payload.total_files;
         if (totalFiles > 0) {
-          totalProgress = (completedFiles / totalFiles) * 100;
+          const currentFileFraction =
+            payload.total > 0 ? payload.downloaded / payload.total : 0;
+          totalProgress = ((completedFiles + currentFileFraction) / totalFiles) * 100;
         }
 
         // Calculate download speed (using moving average)
-- 
cgit v1.2.3-70-g09d2


From e6295ae4e80e4eb31163a86815d0257ce2091d3b Mon Sep 17 00:00:00 2001
From: 简律纯 <i@jyunko.cn>
Date: Wed, 14 Jan 2026 13:52:15 +0800
Subject: Update ui/src/lib/DownloadMonitor.svelte

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
 ui/src/lib/DownloadMonitor.svelte | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

(limited to 'ui/src/lib/DownloadMonitor.svelte')

diff --git a/ui/src/lib/DownloadMonitor.svelte b/ui/src/lib/DownloadMonitor.svelte
index 5dc8269..79adff5 100644
--- a/ui/src/lib/DownloadMonitor.svelte
+++ b/ui/src/lib/DownloadMonitor.svelte
@@ -83,12 +83,19 @@
         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;
+          // On the first update, initialize lastTotalBytes to avoid counting
+          // bytes downloaded before the timer window started.
+          if (lastTotalBytes === 0 && totalDownloadedBytes > 0) {
+            lastTotalBytes = totalDownloadedBytes;
+            lastUpdateTime = now;
+          } else {
+            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
-- 
cgit v1.2.3-70-g09d2


From 2e1a90fa5681d4ef40a8675820300717d8294de0 Mon Sep 17 00:00:00 2001
From: 简律纯 <i@jyunko.cn>
Date: Wed, 14 Jan 2026 13:52:46 +0800
Subject: Update ui/src/lib/DownloadMonitor.svelte

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
 ui/src/lib/DownloadMonitor.svelte | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

(limited to 'ui/src/lib/DownloadMonitor.svelte')

diff --git a/ui/src/lib/DownloadMonitor.svelte b/ui/src/lib/DownloadMonitor.svelte
index 79adff5..9adef67 100644
--- a/ui/src/lib/DownloadMonitor.svelte
+++ b/ui/src/lib/DownloadMonitor.svelte
@@ -98,12 +98,19 @@
           }
         }
 
-        // Estimate remaining time based on files remaining
+        // Estimate remaining time
         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;
+          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;
-- 
cgit v1.2.3-70-g09d2

