Queue system & more

This commit is contained in:
2025-07-21 17:07:02 -07:00
parent 8aeaa997be
commit 010c47743a
6 changed files with 109 additions and 30 deletions

1
src-tauri/Cargo.lock generated
View File

@@ -308,7 +308,6 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
name = "berry-dash-launcher" name = "berry-dash-launcher"
version = "1.0.0" version = "1.0.0"
dependencies = [ dependencies = [
"base64 0.22.1",
"futures-util", "futures-util",
"libc", "libc",
"reqwest", "reqwest",

View File

@@ -17,7 +17,6 @@ tauri-plugin-opener = "2.4.0"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141" serde_json = "1.0.141"
tauri-plugin-os = "2.3.0" tauri-plugin-os = "2.3.0"
base64 = "0.22.1"
reqwest = { version = "0.12.22", features = ["stream"] } reqwest = { version = "0.12.22", features = ["stream"] }
tokio = "1.46.1" tokio = "1.46.1"
futures-util = { version = "0.3.31", features = ["io"] } futures-util = { version = "0.3.31", features = ["io"] }

View File

@@ -1,15 +1,14 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
use base64::{Engine, engine::general_purpose};
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use std::{ use std::{
fs::{File, create_dir_all}, fs::{create_dir_all, File},
io::{BufReader, copy}, io::{copy, BufReader},
path::PathBuf, path::PathBuf,
process::Command, process::Command, time::Duration,
}; };
use tauri::{AppHandle, Emitter, Manager}; use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; 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; use zip::ZipArchive;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@@ -50,10 +49,16 @@ async fn download(
name: String, name: String,
executable: String, executable: String,
) -> Result<(), String> { ) -> Result<(), String> {
app.emit("download-started", &url).unwrap(); app.emit("download-started", &name).unwrap();
let client = reqwest::Client::new(); 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 total_size = resp.content_length().unwrap_or(0);
let mut downloaded: u64 = 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 _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
let mut file = tokio::fs::File::create(download_part_path).await.unwrap(); let mut file = tokio::fs::File::create(download_part_path).await.unwrap();
while let Some(chunk) = stream.next().await { while let Ok(Some(chunk_result)) = timeout(Duration::from_secs(5), stream.next()).await {
let chunk = chunk.map_err(|e| e.to_string())?; let chunk = match chunk_result {
file.write_all(&chunk).await.map_err(|e| e.to_string())?; 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; downloaded += chunk.len() as u64;
let progress = if total_size > 0 { let progress = if total_size > 0 {
@@ -83,13 +99,17 @@ async fn download(
} else { } else {
0 0
}; };
app.emit(
"download-progress", app.emit("download-progress", format!("{}:{}", &name, progress)).unwrap();
format!("{}:{}", general_purpose::STANDARD.encode(&url), 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( tokio::fs::rename(
downloads_path.join(format!("{}.part", name)), downloads_path.join(format!("{}.part", name)),
download_zip_path.clone(), download_zip_path.clone(),
@@ -110,7 +130,7 @@ async fn download(
fs::set_permissions(executable_path, perms).unwrap(); fs::set_permissions(executable_path, perms).unwrap();
} }
app.emit("download-finished", &url).unwrap(); app.emit("download-complete", &name).unwrap();
Ok(()) Ok(())
} }

View File

@@ -21,28 +21,67 @@ function App () {
const [showPopup, setShowPopup] = useState(false) const [showPopup, setShowPopup] = useState(false)
const [popupMode, setPopupMode] = useState<null | number>(null) const [popupMode, setPopupMode] = useState<null | number>(null)
const [fadeOut, setFadeOut] = useState(false) 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<string>('download-progress', (event) => { listen<string>('download-progress', (event) => {
const [urlEnc, progStr] = event.payload.split(':') const [versionName, progStr] = event.payload.split(':')
const url = atob(urlEnc)
const prog = Number(progStr) const prog = Number(progStr)
setDownloadProgress(prev => { 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 (i === -1) return prev
if (prog >= 100) return prev.filter((_, j) => j !== i)
const copy = [...prev] const copy = [...prev]
copy[i] = { ...copy[i], progress: prog } copy[i] = { ...copy[i], progress: prog }
return copy return copy
}) })
}) })
listen<string>('download-done', (event) => {
const versionName = event.payload
setDownloadProgress(prev => prev.filter(d => d.version.version !== versionName))
activeDownloads--
runNext()
})
listen<string>('download-failed', (event) => {
const versionName = event.payload
setDownloadProgress(prev => prev.filter(d => d.version.version !== versionName))
activeDownloads--
runNext()
})
function downloadVersions(versions: LauncherVersion[]) { function downloadVersions(versions: LauncherVersion[]) {
const newDownloads = versions.map(v => new DownloadProgress(v, 0, false)); const newDownloads = versions.map(v => new DownloadProgress(v, 0, false, true))
setDownloadProgress(prev => [...prev, ...newDownloads]); setDownloadProgress(prev => [...prev, ...newDownloads])
newDownloads.forEach(download => { 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<HTMLDivElement>) { function handleOverlayClick (e: React.MouseEvent<HTMLDivElement>) {
@@ -126,9 +165,29 @@ function App () {
<p className='text-center mt-6'>Nothing here...</p> <p className='text-center mt-6'>Nothing here...</p>
) : ( ) : (
downloadProgress.map((v, i) => ( downloadProgress.map((v, i) => (
<div key={i} className='popup-entry'> <div key={i} className='popup-entry flex flex-col justify-between'>
<p className='text-2xl'>Berry Dash v{v.version.displayName}</p> <p className='text-2xl'>Berry Dash v{v.version.displayName}</p>
<p className='mt-[25px]'>{v.progress}% downloaded</p> <div className='mt-[25px] flex items-center justify-between'>
{v.failed ? (
<>
<div className='flex items-center'>
<span className='text-red-500'>Download failed</span>
<button
className='button ml-30 mb-2'
onClick={() =>
setDownloadProgress((prev) => prev.filter((_, idx) => idx !== i))
}
>
Cancel
</button>
</div>
</>
) : v.queued ? (
<span className='text-yellow-500'>Queued</span>
) : (
<span>Downloading: {v.progress}% done</span>
)}
</div>
</div> </div>
)) ))
)} )}

View File

@@ -4,6 +4,7 @@ export class DownloadProgress {
constructor ( constructor (
public version: LauncherVersion, public version: LauncherVersion,
public progress: number, public progress: number,
public failed: boolean public failed: boolean,
public queued: boolean
) {} ) {}
} }

View File

@@ -2,5 +2,6 @@ export interface LauncherVersion {
version: string version: string
displayName: string displayName: string
platforms: string[] platforms: string[]
downloadUrls: string[] downloadUrls: string[],
executables: string[]
} }