feat(desktop): stream download progress and auto-launch installer

This commit is contained in:
2026-02-18 22:24:41 +08:00
parent e4098bfda9
commit b931f36bde
2 changed files with 231 additions and 6 deletions

View File

@@ -7,7 +7,9 @@ use std::fs;
use std::io::Write;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::time::{Duration, UNIX_EPOCH};
use std::process::Command;
use std::time::{Duration, Instant, UNIX_EPOCH};
use tauri::Emitter;
struct ApiState {
client: reqwest::Client,
@@ -21,6 +23,47 @@ struct BridgeResponse {
data: Value,
}
#[derive(Debug, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct NativeDownloadProgressPayload {
task_id: String,
downloaded_bytes: u64,
total_bytes: Option<u64>,
progress: Option<f64>,
resumed_bytes: u64,
done: bool,
}
fn emit_native_download_progress(
window: &tauri::WebviewWindow,
task_id: &str,
downloaded_bytes: u64,
total_bytes: Option<u64>,
resumed_bytes: u64,
done: bool,
) {
if task_id.trim().is_empty() {
return;
}
let progress = total_bytes
.filter(|total| *total > 0)
.map(|total| (downloaded_bytes as f64 / total as f64) * 100.0);
let payload = NativeDownloadProgressPayload {
task_id: task_id.to_string(),
downloaded_bytes,
total_bytes,
progress,
resumed_bytes,
done,
};
if let Err(err) = window.emit("native-download-progress", payload) {
eprintln!("emit native-download-progress failed: {}", err);
}
}
fn normalize_base_url(base_url: &str) -> String {
let trimmed = base_url.trim();
if trimmed.is_empty() {
@@ -550,8 +593,10 @@ async fn api_create_direct_link(
#[tauri::command]
async fn api_native_download(
state: tauri::State<'_, ApiState>,
window: tauri::WebviewWindow,
url: String,
file_name: Option<String>,
task_id: Option<String>,
) -> Result<BridgeResponse, String> {
let trimmed_url = url.trim().to_string();
if trimmed_url.is_empty() {
@@ -595,6 +640,16 @@ async fn api_native_download(
let save_path = alloc_download_path(&download_dir, preferred_name);
fs::rename(&resume_temp_path, &save_path)
.map_err(|err| format!("完成断点下载失败: {}", err))?;
if let Some(ref id) = task_id {
emit_native_download_progress(
&window,
id,
existing_size,
Some(existing_size),
existing_size,
true,
);
}
let mut data = Map::new();
data.insert("success".to_string(), Value::Bool(true));
data.insert(
@@ -625,6 +680,26 @@ async fn api_native_download(
}
let append_mode = existing_size > 0 && status == reqwest::StatusCode::PARTIAL_CONTENT;
let total_bytes = if append_mode {
response
.content_length()
.map(|remaining| remaining.saturating_add(existing_size))
} else {
response.content_length()
};
let resumed_bytes = if append_mode { existing_size } else { 0 };
if let Some(ref id) = task_id {
emit_native_download_progress(
&window,
id,
if append_mode { existing_size } else { 0 },
total_bytes,
resumed_bytes,
false,
);
}
if !append_mode && resume_temp_path.exists() {
fs::remove_file(&resume_temp_path)
.map_err(|err| format!("重置断点下载文件失败: {}", err))?;
@@ -640,6 +715,7 @@ async fn api_native_download(
let mut downloaded_bytes: u64 = if append_mode { existing_size } else { 0 };
let mut stream = response;
let mut last_emit = Instant::now();
while let Some(chunk) = stream
.chunk()
.await
@@ -649,6 +725,20 @@ async fn api_native_download(
.write_all(&chunk)
.map_err(|err| format!("写入文件失败: {}", err))?;
downloaded_bytes += chunk.len() as u64;
if let Some(ref id) = task_id {
if last_emit.elapsed() >= Duration::from_millis(120) {
emit_native_download_progress(
&window,
id,
downloaded_bytes,
total_bytes,
resumed_bytes,
false,
);
last_emit = Instant::now();
}
}
}
target_file
@@ -659,6 +749,17 @@ async fn api_native_download(
fs::rename(&resume_temp_path, &save_path)
.map_err(|err| format!("保存下载文件失败: {}", err))?;
if let Some(ref id) = task_id {
emit_native_download_progress(
&window,
id,
downloaded_bytes,
total_bytes.or(Some(downloaded_bytes)),
resumed_bytes,
true,
);
}
let mut data = Map::new();
data.insert("success".to_string(), Value::Bool(true));
data.insert(
@@ -681,6 +782,44 @@ async fn api_native_download(
})
}
#[tauri::command]
fn api_launch_installer(installer_path: String) -> Result<BridgeResponse, String> {
let path_text = installer_path.trim().to_string();
if path_text.is_empty() {
return Err("安装包路径不能为空".to_string());
}
let installer = PathBuf::from(&path_text);
if !installer.exists() {
return Err("安装包不存在,请重新下载".to_string());
}
if !installer.is_file() {
return Err("安装包路径无效".to_string());
}
#[cfg(target_os = "windows")]
let spawn_result = Command::new(&installer).spawn();
#[cfg(target_os = "macos")]
let spawn_result = Command::new("open").arg(&installer).spawn();
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
let spawn_result = Command::new("xdg-open").arg(&installer).spawn();
spawn_result.map_err(|err| format!("启动安装程序失败: {}", err))?;
let mut data = Map::new();
data.insert("success".to_string(), Value::Bool(true));
data.insert("message".to_string(), Value::String("安装程序已启动".to_string()));
data.insert("installerPath".to_string(), Value::String(path_text));
Ok(BridgeResponse {
ok: true,
status: 200,
data: Value::Object(data),
})
}
#[tauri::command]
async fn api_check_client_update(
state: tauri::State<'_, ApiState>,
@@ -1060,6 +1199,7 @@ pub fn run() {
api_delete_share,
api_create_direct_link,
api_native_download,
api_launch_installer,
api_check_client_update,
api_list_local_files,
api_upload_file_resumable,