Queue system & more
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
src/main.tsx
79
src/main.tsx
@@ -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>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user