mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-23 08:32:51 +00:00
feat: parallel downloads, redesigned download overlay (#100)
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
use std::fs;
|
||||
use serde::Serialize;
|
||||
use tauri::AppHandle;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use crate::util;
|
||||
use super::download::DownloadProgress;
|
||||
#[derive(Serialize)]
|
||||
pub struct GitEntry {
|
||||
pub name: String,
|
||||
@@ -180,7 +181,8 @@ pub async fn download_dlc_files(
|
||||
|
||||
fs::create_dir_all(&dlc_dest).map_err(|e| e.to_string())?;
|
||||
let client = reqwest::Client::new();
|
||||
for file_path in &files_to_download {
|
||||
let total = files_to_download.len();
|
||||
for (i, file_path) in files_to_download.iter().enumerate() {
|
||||
let raw_url = get_raw_url(&host, &owner, &repo, &branch, file_path, is_github);
|
||||
let response = client.get(&raw_url)
|
||||
.header("User-Agent", "Emerald-Launcher")
|
||||
@@ -199,6 +201,10 @@ pub async fn download_dlc_files(
|
||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||
}
|
||||
fs::write(&dest_path, &bytes).map_err(|e| format!("Failed to write {}: {}", file_path, e))?;
|
||||
let _ = app.emit("download-progress", DownloadProgress {
|
||||
instance_id: format!("dlc:{}", dlc_folder),
|
||||
percent: ((i + 1) as f64 / total as f64) * 100.0,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,30 +2,35 @@ use std::fs;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
use futures_util::StreamExt;
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use crate::state::DownloadState;
|
||||
use crate::util;
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DownloadProgress {
|
||||
pub instance_id: String,
|
||||
pub percent: f64,
|
||||
}
|
||||
|
||||
async fn stream_download(
|
||||
app: &AppHandle,
|
||||
state: &DownloadState,
|
||||
url: &str,
|
||||
dest: &std::path::PathBuf,
|
||||
progress_event: &str,
|
||||
instance_id: &str,
|
||||
) -> Result<String, String> {
|
||||
let token = CancellationToken::new();
|
||||
let child_token = token.clone();
|
||||
{
|
||||
let mut lock = state.token.lock().await;
|
||||
if let Some(old_token) = lock.take() {
|
||||
old_token.cancel();
|
||||
}
|
||||
*lock = Some(token);
|
||||
let mut lock = state.tokens.lock().await;
|
||||
lock.insert(instance_id.to_string(), token);
|
||||
}
|
||||
|
||||
let response = reqwest::get(url).await.map_err(|e| e.to_string())?;
|
||||
if !response.status().is_success() {
|
||||
{ *state.token.lock().await = None; }
|
||||
{ state.tokens.lock().await.remove(instance_id); }
|
||||
return Err(format!("Download failed: {}", response.status()));
|
||||
}
|
||||
|
||||
@@ -41,19 +46,22 @@ async fn stream_download(
|
||||
if child_token.is_cancelled() {
|
||||
drop(file);
|
||||
let _ = fs::remove_file(dest);
|
||||
{ *state.token.lock().await = None; }
|
||||
{ state.tokens.lock().await.remove(instance_id); }
|
||||
return Err("CANCELLED".into());
|
||||
}
|
||||
let chunk = chunk.map_err(|e| e.to_string())?;
|
||||
file.write_all(&chunk).map_err(|e| e.to_string())?;
|
||||
downloaded += chunk.len() as f64;
|
||||
if total_size > 0.0 {
|
||||
let _ = app.emit(progress_event, downloaded / total_size * 100.0);
|
||||
let _ = app.emit("download-progress", DownloadProgress {
|
||||
instance_id: instance_id.to_string(),
|
||||
percent: downloaded / total_size * 100.0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
drop(file);
|
||||
{ *state.token.lock().await = None; }
|
||||
{ state.tokens.lock().await.remove(instance_id); }
|
||||
Ok(last_modified)
|
||||
}
|
||||
|
||||
@@ -62,7 +70,7 @@ async fn download_with_retry(
|
||||
state: &DownloadState,
|
||||
url: &str,
|
||||
dest: &std::path::PathBuf,
|
||||
progress_event: &str,
|
||||
instance_id: &str,
|
||||
max_retries: u32,
|
||||
) -> Result<String, String> {
|
||||
let mut last_error = String::new();
|
||||
@@ -71,8 +79,8 @@ async fn download_with_retry(
|
||||
let backoff = Duration::from_secs(2u64.pow(attempt - 2));
|
||||
let cancel = CancellationToken::new();
|
||||
{
|
||||
let mut lock = state.token.lock().await;
|
||||
*lock = Some(cancel.clone());
|
||||
let mut lock = state.tokens.lock().await;
|
||||
lock.insert(instance_id.to_string(), cancel.clone());
|
||||
}
|
||||
let _ = app.emit("download-retry", attempt);
|
||||
tokio::select! {
|
||||
@@ -83,7 +91,7 @@ async fn download_with_retry(
|
||||
}
|
||||
}
|
||||
}
|
||||
match stream_download(app, state, url, dest, progress_event).await {
|
||||
match stream_download(app, state, url, dest, instance_id).await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) if e == "CANCELLED" => return Err(e),
|
||||
Err(e) => {
|
||||
@@ -107,7 +115,7 @@ pub async fn download_and_install(
|
||||
let instance_dir = util::get_instance_working_dir(&app, &instance_id);
|
||||
let root = util::get_app_dir(&app);
|
||||
let zip_path = root.join(format!("temp_{}.zip", instance_id));
|
||||
let last_modified = download_with_retry(&app, &state, &url, &zip_path, "download-progress", 3).await?;
|
||||
let last_modified = download_with_retry(&app, &state, &url, &zip_path, &instance_id, 3).await?;
|
||||
let keep_list: std::collections::HashSet<&str> = [
|
||||
"Windows64", "Windows64Media", "uid.dat", "username.txt", "settings.dat",
|
||||
"servers.dat", "servers.txt", "server.properties", "options.txt", "servers.db",
|
||||
@@ -226,8 +234,11 @@ pub async fn download_and_install(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn cancel_download(state: State<'_, DownloadState>) -> Result<(), String> {
|
||||
if let Some(token) = state.token.lock().await.take() { token.cancel(); }
|
||||
pub async fn cancel_download(state: State<'_, DownloadState>, instance_id: String) -> Result<(), String> {
|
||||
let token = state.tokens.lock().await.remove(&instance_id);
|
||||
if let Some(t) = token {
|
||||
t.cancel();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -245,10 +256,9 @@ pub async fn download_runner(
|
||||
let _ = fs::remove_dir_all(&runner_dir);
|
||||
}
|
||||
fs::create_dir_all(&runner_dir).map_err(|e| e.to_string())?;
|
||||
|
||||
let tarball_path = runners_dir.join(format!("{}.tar.gz", name));
|
||||
download_with_retry(&app, &state, &url, &tarball_path, "runner-download-progress", 3).await?;
|
||||
|
||||
let runner_id = format!("runner_{}", name);
|
||||
download_with_retry(&app, &state, &url, &tarball_path, &runner_id, 3).await?;
|
||||
let status = std::process::Command::new("tar")
|
||||
.args(["-zxf", tarball_path.to_str().unwrap(), "-C", runner_dir.to_str().unwrap(), "--strip-components=1"])
|
||||
.status()
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.manage(DownloadState {
|
||||
token: Arc::new(Mutex::new(None)),
|
||||
tokens: Arc::new(Mutex::new(HashMap::new())),
|
||||
})
|
||||
.manage(GameState {
|
||||
child: Arc::new(Mutex::new(None)),
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
pub struct DownloadState {
|
||||
pub token: Arc<Mutex<Option<CancellationToken>>>,
|
||||
pub tokens: Arc<Mutex<HashMap<String, CancellationToken>>>,
|
||||
}
|
||||
|
||||
pub struct GameState {
|
||||
|
||||
Reference in New Issue
Block a user