feat(desktop): implement advanced transfer, tray icon, media cookies, and extension tcp server

This commit is contained in:
NimBold
2026-06-13 11:24:41 +03:30
parent 6de0cd268c
commit 9119cafdcd
7 changed files with 180 additions and 5 deletions
@@ -0,0 +1,57 @@
use std::net::TcpListener;
use std::io::{Read, Write};
use std::thread;
use tauri::{AppHandle, Emitter};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Clone)]
struct ExtensionPayload {
url: String,
token: String,
}
pub fn start_server(app_handle: AppHandle) {
thread::spawn(move || {
let listener = match TcpListener::bind("127.0.0.1:23522") {
Ok(l) => l,
Err(_) => return, // Port might be in use, ignore for now
};
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let mut buffer = [0; 1024];
if let Ok(size) = stream.read(&mut buffer) {
let request = String::from_utf8_lossy(&buffer[..size]);
// Parse simple HTTP POST
if request.starts_with("POST") {
if let Some(body_start) = request.find("\r\n\r\n") {
let body = &request[body_start + 4..];
if let Ok(payload) = serde_json::from_str::<ExtensionPayload>(body) {
// Emit event to frontend
let _ = app_handle.emit("extension-add-download", payload);
// Send success response
let response = "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: application/json\r\n\r\n{\"success\":true}";
let _ = stream.write_all(response.as_bytes());
continue;
}
}
}
// Handle OPTIONS for CORS
if request.starts_with("OPTIONS") {
let response = "HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n\r\n";
let _ = stream.write_all(response.as_bytes());
} else {
let response = "HTTP/1.1 400 Bad Request\r\nAccess-Control-Allow-Origin: *\r\n\r\n";
let _ = stream.write_all(response.as_bytes());
}
}
}
Err(_) => {}
}
}
});
}
+76 -1
View File
@@ -371,6 +371,9 @@ async fn start_download(
username: Option<String>,
password: Option<String>,
headers: Option<String>,
checksum: Option<String>,
cookies: Option<String>,
mirrors: Option<String>,
user_agent: Option<String>,
max_tries: Option<i32>,
proxy: Option<String>,
@@ -432,6 +435,16 @@ async fn start_download(
cmd.arg(format!("--header={}", hdr));
}
}
if let Some(chk) = checksum {
if !chk.is_empty() {
cmd.arg(format!("--checksum={}", chk));
}
}
if let Some(cks) = cookies {
if !cks.is_empty() {
cmd.arg(format!("--header=Cookie: {}", cks));
}
}
if let Some(ua) = user_agent {
if !ua.is_empty() {
cmd.arg(format!("--user-agent={}", ua));
@@ -447,6 +460,18 @@ async fn start_download(
}
cmd.arg(&url);
if let Some(m) = mirrors {
if !m.is_empty() {
for mirror_url in m.lines() {
let trimmed = mirror_url.trim();
if !trimmed.is_empty() {
cmd.arg(trimmed);
}
}
}
}
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::null());
// We remove kill_on_drop(true) so we can gracefully SIGTERM it
@@ -527,6 +552,7 @@ async fn start_media_download(
destination: String,
filename: String,
format_selector: Option<String>,
cookie_source: Option<String>,
speed_limit: Option<String>,
username: Option<String>,
password: Option<String>,
@@ -575,6 +601,12 @@ async fn start_media_download(
}
}
if let Some(cs) = cookie_source {
if !cs.is_empty() && cs != "none" {
cmd.arg("--cookies-from-browser").arg(cs);
}
}
if let Some(format) = format_selector {
cmd.arg("-f").arg(format);
// If the filename implies an audio format, use it as audio output
@@ -958,9 +990,51 @@ fn delete_file(app_handle: tauri::AppHandle, path: String) -> Result<(), String>
}
}
#[tauri::command]
fn toggle_tray_icon(app_handle: tauri::AppHandle, show: bool) -> Result<(), String> {
use tauri::tray::TrayIconBuilder;
use tauri::menu::{Menu, MenuItem};
use tauri::Manager;
if show {
if app_handle.tray_by_id("main").is_none() {
let quit_i = MenuItem::with_id(&app_handle, "quit", "Quit", true, None::<&str>).map_err(|e| e.to_string())?;
let show_i = MenuItem::with_id(&app_handle, "show", "Show Firelink", true, None::<&str>).map_err(|e| e.to_string())?;
let menu = Menu::with_items(&app_handle, &[&show_i, &quit_i]).map_err(|e| e.to_string())?;
let _tray = TrayIconBuilder::with_id("main")
.icon(app_handle.default_window_icon().unwrap().clone())
.menu(&menu)
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => {
std::process::exit(0);
}
"show" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
_ => {}
})
.build(&app_handle)
.map_err(|e| e.to_string())?;
}
} else {
if let Some(_tray) = app_handle.tray_by_id("main") {
let _ = app_handle.remove_tray_by_id("main");
}
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
extension_server::start_server(app.handle().clone());
Ok(())
})
.manage(AppState {
tasks: Mutex::new(HashMap::new()),
})
@@ -973,8 +1047,9 @@ pub fn run() {
update_dock_badge, set_prevent_sleep, get_free_space, perform_system_action,
request_automation_permission, open_automation_settings,
set_keychain_password, get_keychain_password, delete_keychain_password,
check_file_exists, delete_file
check_file_exists, delete_file, toggle_tray_icon
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
mod extension_server;