From 010c47743a12dbb98aac6a10681f38997f3377ad Mon Sep 17 00:00:00 2001 From: Lncvrt Date: Mon, 21 Jul 2025 17:07:02 -0700 Subject: [PATCH] Queue system & more --- src-tauri/Cargo.lock | 1 - src-tauri/Cargo.toml | 1 - src-tauri/src/lib.rs | 52 ++++++++++++++++------- src/main.tsx | 79 ++++++++++++++++++++++++++++++----- src/types/DownloadProgress.ts | 3 +- src/types/LauncherVersion.ts | 3 +- 6 files changed, 109 insertions(+), 30 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9c67009..a0f08c7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -308,7 +308,6 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" name = "berry-dash-launcher" version = "1.0.0" dependencies = [ - "base64 0.22.1", "futures-util", "libc", "reqwest", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 040fb7e..8a994e5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -17,7 +17,6 @@ tauri-plugin-opener = "2.4.0" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.141" tauri-plugin-os = "2.3.0" -base64 = "0.22.1" reqwest = { version = "0.12.22", features = ["stream"] } tokio = "1.46.1" futures-util = { version = "0.3.31", features = ["io"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 650b629..8e2598f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,15 +1,14 @@ #[cfg_attr(mobile, tauri::mobile_entry_point)] -use base64::{Engine, engine::general_purpose}; use futures_util::stream::StreamExt; use std::{ - fs::{File, create_dir_all}, - io::{BufReader, copy}, + fs::{create_dir_all, File}, + io::{copy, BufReader}, path::PathBuf, - process::Command, + process::Command, time::Duration, }; use tauri::{AppHandle, Emitter, Manager}; use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; -use tokio::{io::AsyncWriteExt, task::spawn_blocking}; +use tokio::{io::AsyncWriteExt, task::spawn_blocking, time::timeout}; use zip::ZipArchive; #[cfg(target_os = "linux")] @@ -50,10 +49,16 @@ async fn download( name: String, executable: String, ) -> Result<(), String> { - app.emit("download-started", &url).unwrap(); + app.emit("download-started", &name).unwrap(); let client = reqwest::Client::new(); - let resp = client.get(&url).send().await.map_err(|e| e.to_string())?; + let resp = match client.get(&url).send().await { + Ok(r) => r, + Err(e) => { + app.emit("download-failed", &name).unwrap(); + return Err(e.to_string()); + } + }; let total_size = resp.content_length().unwrap_or(0); let mut downloaded: u64 = 0; @@ -73,9 +78,20 @@ async fn download( let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await; let mut file = tokio::fs::File::create(download_part_path).await.unwrap(); - while let Some(chunk) = stream.next().await { - let chunk = chunk.map_err(|e| e.to_string())?; - file.write_all(&chunk).await.map_err(|e| e.to_string())?; + while let Ok(Some(chunk_result)) = timeout(Duration::from_secs(5), stream.next()).await { + let chunk = match chunk_result { + Ok(c) => c, + Err(e) => { + app.emit("download-failed", &name).unwrap(); + return Err(e.to_string()); + } + }; + + if let Err(e) = file.write_all(&chunk).await { + app.emit("download-failed", &name).unwrap(); + return Err(e.to_string()); + } + downloaded += chunk.len() as u64; let progress = if total_size > 0 { @@ -83,13 +99,17 @@ async fn download( } else { 0 }; - app.emit( - "download-progress", - format!("{}:{}", general_purpose::STANDARD.encode(&url), progress), - ) - .unwrap(); + + app.emit("download-progress", format!("{}:{}", &name, progress)).unwrap(); } + if total_size > 0 && downloaded < total_size { + app.emit("download-failed", &name).unwrap(); + return Err("Download incomplete".into()); + } + + app.emit("download-done", &name).unwrap(); + tokio::fs::rename( downloads_path.join(format!("{}.part", name)), download_zip_path.clone(), @@ -110,7 +130,7 @@ async fn download( fs::set_permissions(executable_path, perms).unwrap(); } - app.emit("download-finished", &url).unwrap(); + app.emit("download-complete", &name).unwrap(); Ok(()) } diff --git a/src/main.tsx b/src/main.tsx index 0ff2023..a4b4571 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -21,28 +21,67 @@ function App () { const [showPopup, setShowPopup] = useState(false) const [popupMode, setPopupMode] = useState(null) const [fadeOut, setFadeOut] = useState(false) + let activeDownloads = 0 + const queue: (() => void)[] = [] + + function runNext() { + if (activeDownloads >= 3 || queue.length === 0) return + activeDownloads++ + const next = queue.shift() + next?.() + } listen('download-progress', (event) => { - const [urlEnc, progStr] = event.payload.split(':') - const url = atob(urlEnc) + const [versionName, progStr] = event.payload.split(':') const prog = Number(progStr) + setDownloadProgress(prev => { - const i = prev.findIndex(d => d.version.downloadUrls[d.version.platforms.indexOf(platform())] === url) + const i = prev.findIndex(d => d.version.version === versionName) if (i === -1) return prev - if (prog >= 100) return prev.filter((_, j) => j !== i) const copy = [...prev] copy[i] = { ...copy[i], progress: prog } return copy }) }) + listen('download-done', (event) => { + const versionName = event.payload + setDownloadProgress(prev => prev.filter(d => d.version.version !== versionName)) + activeDownloads-- + runNext() + }) + + listen('download-failed', (event) => { + const versionName = event.payload + setDownloadProgress(prev => prev.filter(d => d.version.version !== versionName)) + activeDownloads-- + runNext() + }) + function downloadVersions(versions: LauncherVersion[]) { - const newDownloads = versions.map(v => new DownloadProgress(v, 0, false)); - setDownloadProgress(prev => [...prev, ...newDownloads]); + const newDownloads = versions.map(v => new DownloadProgress(v, 0, false, true)) + setDownloadProgress(prev => [...prev, ...newDownloads]) newDownloads.forEach(download => { - invoke('download', { url: download.version.downloadUrls[download.version.platforms.indexOf(platform())], name: download.version.version }); - }); + const task = () => { + setDownloadProgress(prev => { + const i = prev.findIndex(d => d.version.version === download.version.version) + if (i === -1) return prev + const copy = [...prev] + copy[i] = { ...copy[i], queued: false } + return copy + }) + + invoke('download', { + url: download.version.downloadUrls[download.version.platforms.indexOf(platform())], + name: download.version.version, + executable: download.version.executables[download.version.platforms.indexOf(platform())] + }) + } + + queue.push(task) + runNext() + }) } function handleOverlayClick (e: React.MouseEvent) { @@ -126,9 +165,29 @@ function App () {

Nothing here...

) : ( downloadProgress.map((v, i) => ( -
+

Berry Dash v{v.version.displayName}

-

{v.progress}% downloaded

+
+ {v.failed ? ( + <> +
+ Download failed + +
+ + ) : v.queued ? ( + Queued… + ) : ( + Downloading: {v.progress}% done + )} +
)) )} diff --git a/src/types/DownloadProgress.ts b/src/types/DownloadProgress.ts index 763ca2d..e13aa82 100644 --- a/src/types/DownloadProgress.ts +++ b/src/types/DownloadProgress.ts @@ -4,6 +4,7 @@ export class DownloadProgress { constructor ( public version: LauncherVersion, public progress: number, - public failed: boolean + public failed: boolean, + public queued: boolean ) {} } diff --git a/src/types/LauncherVersion.ts b/src/types/LauncherVersion.ts index 3a6aefa..c17e998 100644 --- a/src/types/LauncherVersion.ts +++ b/src/types/LauncherVersion.ts @@ -2,5 +2,6 @@ export interface LauncherVersion { version: string displayName: string platforms: string[] - downloadUrls: string[] + downloadUrls: string[], + executables: string[] }