mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-20 16:12:17 +00:00
fix(infra): harden loopback server lifecycle
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
body::Bytes,
|
body::Bytes,
|
||||||
extract::State,
|
extract::State,
|
||||||
http::{HeaderMap, StatusCode, Method},
|
http::{HeaderMap, Method, StatusCode},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
@@ -15,12 +15,17 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
use tauri_plugin_store::StoreExt;
|
||||||
|
use tokio::sync::watch;
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
|
|
||||||
pub const EXTENSION_SERVER_PORT: u16 = 23522;
|
pub const EXTENSION_SERVER_PORT: u16 = 23522;
|
||||||
|
pub const EXTENSION_SERVER_PORT_RANGE: std::ops::RangeInclusive<u16> =
|
||||||
|
EXTENSION_SERVER_PORT..=23531;
|
||||||
const MAX_URL_COUNT: usize = 200;
|
const MAX_URL_COUNT: usize = 200;
|
||||||
const SIGNATURE_MAX_AGE_MS: u64 = 60_000;
|
const SIGNATURE_MAX_AGE_MS: u64 = 60_000;
|
||||||
|
const MAIN_QUEUE_ID: &str = "00000000-0000-0000-0000-000000000001";
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
pub type SharedExtensionToken = Arc<RwLock<String>>;
|
pub type SharedExtensionToken = Arc<RwLock<String>>;
|
||||||
@@ -44,6 +49,10 @@ struct ExtensionRequest {
|
|||||||
silent: bool,
|
silent: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
filename: Option<String>,
|
filename: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
headers: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
cookies: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, TS)]
|
#[derive(Clone, Serialize, TS)]
|
||||||
@@ -53,12 +62,15 @@ pub struct ExtensionDownload {
|
|||||||
referer: Option<String>,
|
referer: Option<String>,
|
||||||
silent: bool,
|
silent: bool,
|
||||||
filename: Option<String>,
|
filename: Option<String>,
|
||||||
|
headers: Option<String>,
|
||||||
|
cookies: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_server(
|
pub async fn start_server(
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
pairing_token: SharedExtensionToken,
|
pairing_token: SharedExtensionToken,
|
||||||
frontend_ready: SharedFrontendReady,
|
frontend_ready: SharedFrontendReady,
|
||||||
|
mut shutdown_rx: watch::Receiver<bool>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let state = ServerState {
|
let state = ServerState {
|
||||||
app_handle,
|
app_handle,
|
||||||
@@ -81,19 +93,41 @@ pub async fn start_server(
|
|||||||
.layer(cors)
|
.layer(cors)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(("127.0.0.1", EXTENSION_SERVER_PORT))
|
let (port, listener) = bind_extension_listener().await?;
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to bind extension server to port {}: {}", EXTENSION_SERVER_PORT, e))?;
|
|
||||||
|
|
||||||
println!("Browser extension server bound to 127.0.0.1:{}", EXTENSION_SERVER_PORT);
|
println!("Browser extension server bound to 127.0.0.1:{port}");
|
||||||
|
|
||||||
axum::serve(listener, app)
|
axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(async move {
|
||||||
|
if *shutdown_rx.borrow() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let _ = shutdown_rx.changed().await;
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Server error: {}", e))?;
|
.map_err(|e| format!("Server error: {}", e))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn bind_extension_listener() -> Result<(u16, tokio::net::TcpListener), String> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for port in EXTENSION_SERVER_PORT_RANGE {
|
||||||
|
match tokio::net::TcpListener::bind(("127.0.0.1", port)).await {
|
||||||
|
Ok(listener) => return Ok((port, listener)),
|
||||||
|
Err(error) => {
|
||||||
|
errors.push(format!("{port}: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(format!(
|
||||||
|
"Failed to bind extension server in port range {}-{} ({})",
|
||||||
|
EXTENSION_SERVER_PORT,
|
||||||
|
*EXTENSION_SERVER_PORT_RANGE.end(),
|
||||||
|
errors.join("; ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn ping_handler(
|
async fn ping_handler(
|
||||||
State(state): State<ServerState>,
|
State(state): State<ServerState>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
@@ -103,12 +137,18 @@ async fn ping_handler(
|
|||||||
return StatusCode::SERVICE_UNAVAILABLE;
|
return StatusCode::SERVICE_UNAVAILABLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
let signature = match headers.get("x-firelink-signature").and_then(|v| v.to_str().ok()) {
|
let signature = match headers
|
||||||
|
.get("x-firelink-signature")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
{
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return StatusCode::FORBIDDEN,
|
None => return StatusCode::FORBIDDEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
let timestamp_str = match headers.get("x-firelink-timestamp").and_then(|v| v.to_str().ok()) {
|
let timestamp_str = match headers
|
||||||
|
.get("x-firelink-timestamp")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
{
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return StatusCode::FORBIDDEN,
|
None => return StatusCode::FORBIDDEN,
|
||||||
};
|
};
|
||||||
@@ -125,16 +165,18 @@ async fn download_handler(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
body: Bytes,
|
body: Bytes,
|
||||||
) -> Result<StatusCode, StatusCode> {
|
) -> Result<StatusCode, StatusCode> {
|
||||||
if !state.frontend_ready.load(Ordering::Acquire) {
|
let signature = match headers
|
||||||
return Err(StatusCode::SERVICE_UNAVAILABLE);
|
.get("x-firelink-signature")
|
||||||
}
|
.and_then(|v| v.to_str().ok())
|
||||||
|
{
|
||||||
let signature = match headers.get("x-firelink-signature").and_then(|v| v.to_str().ok()) {
|
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Err(StatusCode::FORBIDDEN),
|
None => return Err(StatusCode::FORBIDDEN),
|
||||||
};
|
};
|
||||||
|
|
||||||
let timestamp_str = match headers.get("x-firelink-timestamp").and_then(|v| v.to_str().ok()) {
|
let timestamp_str = match headers
|
||||||
|
.get("x-firelink-timestamp")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
{
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => return Err(StatusCode::FORBIDDEN),
|
None => return Err(StatusCode::FORBIDDEN),
|
||||||
};
|
};
|
||||||
@@ -172,13 +214,136 @@ async fn download_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.app_handle.emit("extension-add-download", download).is_err() {
|
if enqueue_extension_download(&state.app_handle, &download)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(StatusCode::OK)
|
Ok(StatusCode::OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn enqueue_extension_download(
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
download: &ExtensionDownload,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let Some(settings) = read_settings(app_handle) else {
|
||||||
|
return Err("settings unavailable".to_string());
|
||||||
|
};
|
||||||
|
let state = app_handle.state::<crate::AppState>();
|
||||||
|
let mut created_items = Vec::new();
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
for url in &download.urls {
|
||||||
|
let filename = download
|
||||||
|
.filename
|
||||||
|
.as_deref()
|
||||||
|
.filter(|_| download.urls.len() == 1)
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| filename_from_url(url));
|
||||||
|
let category = crate::parity::get_file_category(filename.clone());
|
||||||
|
let category_key = format!("{category:?}");
|
||||||
|
let destination = settings
|
||||||
|
.download_directories
|
||||||
|
.get(&category_key)
|
||||||
|
.cloned()
|
||||||
|
.filter(|path| !path.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| settings.default_download_path.clone());
|
||||||
|
let id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let headers = merge_headers(download.referer.as_deref(), download.headers.as_deref());
|
||||||
|
let item = crate::ipc::DownloadItem {
|
||||||
|
id: id.clone(),
|
||||||
|
url: url.clone(),
|
||||||
|
file_name: filename.clone(),
|
||||||
|
status: crate::ipc::DownloadStatus::Queued,
|
||||||
|
fraction: Some(0.0),
|
||||||
|
speed: Some("-".to_string()),
|
||||||
|
eta: Some("-".to_string()),
|
||||||
|
size: None,
|
||||||
|
category,
|
||||||
|
date_added: chrono::Utc::now().to_rfc3339(),
|
||||||
|
connections: Some(settings.per_server_connections),
|
||||||
|
speed_limit: (!settings.global_speed_limit.trim().is_empty())
|
||||||
|
.then(|| settings.global_speed_limit.clone()),
|
||||||
|
username: None,
|
||||||
|
password: None,
|
||||||
|
headers: headers.clone(),
|
||||||
|
checksum: None,
|
||||||
|
cookies: download.cookies.clone(),
|
||||||
|
mirrors: None,
|
||||||
|
destination: Some(destination.clone()),
|
||||||
|
is_media: Some(false),
|
||||||
|
media_format_selector: None,
|
||||||
|
queue_id: MAIN_QUEUE_ID.to_string(),
|
||||||
|
};
|
||||||
|
let task = crate::queue::EnqueueItem {
|
||||||
|
id,
|
||||||
|
url: url.clone(),
|
||||||
|
destination,
|
||||||
|
filename,
|
||||||
|
connections: Some(settings.per_server_connections),
|
||||||
|
speed_limit: (!settings.global_speed_limit.trim().is_empty())
|
||||||
|
.then(|| settings.global_speed_limit.clone()),
|
||||||
|
username: None,
|
||||||
|
password: None,
|
||||||
|
headers,
|
||||||
|
checksum: None,
|
||||||
|
cookies: download.cookies.clone(),
|
||||||
|
mirrors: None,
|
||||||
|
user_agent: (!settings.custom_user_agent.trim().is_empty())
|
||||||
|
.then(|| settings.custom_user_agent.clone()),
|
||||||
|
max_tries: Some(settings.max_automatic_retries),
|
||||||
|
proxy: None,
|
||||||
|
format_selector: None,
|
||||||
|
cookie_source: None,
|
||||||
|
is_media: Some(false),
|
||||||
|
}
|
||||||
|
.into_task();
|
||||||
|
created_items.push(item);
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.queue_manager.enqueue_many(tasks).await;
|
||||||
|
let _ = app_handle.emit("extension-downloads-queued", created_items);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_settings(app_handle: &AppHandle) -> Option<crate::ipc::PersistedSettings> {
|
||||||
|
let store = app_handle.store("store.bin").ok()?;
|
||||||
|
let settings_value = store.get("settings")?;
|
||||||
|
let settings_text = settings_value.as_str()?;
|
||||||
|
serde_json::from_str(settings_text).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_headers(referer: Option<&str>, headers: Option<&str>) -> Option<String> {
|
||||||
|
let mut values = Vec::new();
|
||||||
|
if let Some(referer) = referer {
|
||||||
|
values.push(format!("Referer: {referer}"));
|
||||||
|
}
|
||||||
|
if let Some(headers) = headers {
|
||||||
|
values.extend(
|
||||||
|
headers
|
||||||
|
.lines()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|line| !line.is_empty())
|
||||||
|
.map(str::to_string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(!values.is_empty()).then(|| values.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filename_from_url(raw_url: &str) -> String {
|
||||||
|
Url::parse(raw_url)
|
||||||
|
.ok()
|
||||||
|
.and_then(|url| {
|
||||||
|
url.path_segments()
|
||||||
|
.and_then(Iterator::last)
|
||||||
|
.and_then(|segment| sanitize_filename(segment))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "download".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_download(payload: ExtensionRequest) -> Option<ExtensionDownload> {
|
fn normalize_download(payload: ExtensionRequest) -> Option<ExtensionDownload> {
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
let urls = payload
|
let urls = payload
|
||||||
@@ -203,6 +368,8 @@ fn normalize_download(payload: ExtensionRequest) -> Option<ExtensionDownload> {
|
|||||||
referer,
|
referer,
|
||||||
silent: payload.silent,
|
silent: payload.silent,
|
||||||
filename,
|
filename,
|
||||||
|
headers: payload.headers.filter(|value| !value.trim().is_empty()),
|
||||||
|
cookies: payload.cookies.filter(|value| !value.trim().is_empty()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -494,6 +494,7 @@ pub struct AppState {
|
|||||||
pub download_coordinator: download::DownloadCoordinator,
|
pub download_coordinator: download::DownloadCoordinator,
|
||||||
pub extension_pairing_token: extension_server::SharedExtensionToken,
|
pub extension_pairing_token: extension_server::SharedExtensionToken,
|
||||||
pub extension_frontend_ready: extension_server::SharedFrontendReady,
|
pub extension_frontend_ready: extension_server::SharedFrontendReady,
|
||||||
|
pub extension_server_shutdown: tokio::sync::watch::Sender<bool>,
|
||||||
pub aria2_port: u16,
|
pub aria2_port: u16,
|
||||||
pub aria2_secret: String,
|
pub aria2_secret: String,
|
||||||
pub media_semaphore: Arc<tokio::sync::Semaphore>,
|
pub media_semaphore: Arc<tokio::sync::Semaphore>,
|
||||||
@@ -1444,6 +1445,7 @@ pub fn run() {
|
|||||||
let server_pairing_token = extension_pairing_token.clone();
|
let server_pairing_token = extension_pairing_token.clone();
|
||||||
let extension_frontend_ready = Arc::new(AtomicBool::new(false));
|
let extension_frontend_ready = Arc::new(AtomicBool::new(false));
|
||||||
let server_frontend_ready = extension_frontend_ready.clone();
|
let server_frontend_ready = extension_frontend_ready.clone();
|
||||||
|
let (extension_server_shutdown_tx, extension_server_shutdown_rx) = tokio::sync::watch::channel(false);
|
||||||
|
|
||||||
let aria2_port = std::net::TcpListener::bind("127.0.0.1:0")
|
let aria2_port = std::net::TcpListener::bind("127.0.0.1:0")
|
||||||
.and_then(|listener| listener.local_addr())
|
.and_then(|listener| listener.local_addr())
|
||||||
@@ -1535,6 +1537,7 @@ pub fn run() {
|
|||||||
download_coordinator: download::DownloadCoordinator::spawn(app.handle().clone()),
|
download_coordinator: download::DownloadCoordinator::spawn(app.handle().clone()),
|
||||||
extension_pairing_token,
|
extension_pairing_token,
|
||||||
extension_frontend_ready,
|
extension_frontend_ready,
|
||||||
|
extension_server_shutdown: extension_server_shutdown_tx.clone(),
|
||||||
aria2_port,
|
aria2_port,
|
||||||
aria2_secret: aria2_secret.clone(),
|
aria2_secret: aria2_secret.clone(),
|
||||||
media_semaphore: Arc::new(tokio::sync::Semaphore::new(3)),
|
media_semaphore: Arc::new(tokio::sync::Semaphore::new(3)),
|
||||||
@@ -1767,6 +1770,7 @@ pub fn run() {
|
|||||||
ext_app_handle,
|
ext_app_handle,
|
||||||
server_pairing_token.clone(),
|
server_pairing_token.clone(),
|
||||||
server_frontend_ready.clone(),
|
server_frontend_ready.clone(),
|
||||||
|
extension_server_shutdown_rx,
|
||||||
).await {
|
).await {
|
||||||
eprintln!("Browser extension server unavailable: {error}");
|
eprintln!("Browser extension server unavailable: {error}");
|
||||||
}
|
}
|
||||||
@@ -1809,8 +1813,14 @@ pub fn run() {
|
|||||||
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media, parity::get_supported_media_domains,
|
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media, parity::get_supported_media_domains,
|
||||||
parity::create_category_directories
|
parity::create_category_directories
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while building tauri application")
|
||||||
|
.run(|app_handle, event| {
|
||||||
|
if let tauri::RunEvent::ExitRequested { .. } = event {
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
let _ = state.extension_server_shutdown.send(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
mod extension_server;
|
mod extension_server;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
|
|||||||
+19
-1
@@ -238,10 +238,27 @@ function App() {
|
|||||||
const unlistenExtension = listen('extension-add-download', (event) => {
|
const unlistenExtension = listen('extension-add-download', (event) => {
|
||||||
useDownloadStore.getState().handleExtensionDownload(event.payload);
|
useDownloadStore.getState().handleExtensionDownload(event.payload);
|
||||||
});
|
});
|
||||||
|
const unlistenExtensionQueued = listen('extension-downloads-queued', (event) => {
|
||||||
|
const store = useDownloadStore.getState();
|
||||||
|
const incoming = event.payload;
|
||||||
|
const existing = new Set(store.downloads.map(download => download.id));
|
||||||
|
const additions = incoming.filter(download => !existing.has(download.id));
|
||||||
|
if (additions.length === 0) return;
|
||||||
|
useDownloadStore.setState(state => ({
|
||||||
|
downloads: [...state.downloads, ...additions],
|
||||||
|
pendingOrder: [
|
||||||
|
...state.pendingOrder,
|
||||||
|
...additions
|
||||||
|
.filter(download => download.status === 'queued')
|
||||||
|
.map(download => download.id)
|
||||||
|
.filter(id => !state.pendingOrder.includes(id)),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
});
|
||||||
const unlistenDeepLink = listen('deep-link-add-download', (event) => {
|
const unlistenDeepLink = listen('deep-link-add-download', (event) => {
|
||||||
useDownloadStore.getState().openAddModalWithUrls(event.payload);
|
useDownloadStore.getState().openAddModalWithUrls(event.payload);
|
||||||
});
|
});
|
||||||
Promise.all([unlistenExtension, unlistenDeepLink])
|
Promise.all([unlistenExtension, unlistenExtensionQueued, unlistenDeepLink])
|
||||||
.then(() => invoke('set_extension_frontend_ready', { ready: true }))
|
.then(() => invoke('set_extension_frontend_ready', { ready: true }))
|
||||||
.catch(error => console.error('Failed to activate browser extension integration:', error));
|
.catch(error => console.error('Failed to activate browser extension integration:', error));
|
||||||
|
|
||||||
@@ -250,6 +267,7 @@ function App() {
|
|||||||
unlistenComplete.then(f => f());
|
unlistenComplete.then(f => f());
|
||||||
unlistenFailed.then(f => f());
|
unlistenFailed.then(f => f());
|
||||||
unlistenExtension.then(f => f());
|
unlistenExtension.then(f => f());
|
||||||
|
unlistenExtensionQueued.then(f => f());
|
||||||
unlistenDeepLink.then(f => f());
|
unlistenDeepLink.then(f => f());
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type ExtensionDownload = { urls: Array<string>, referer: string | null, silent: boolean, filename: string | null, };
|
export type ExtensionDownload = { urls: Array<string>, referer: string | null, silent: boolean, filename: string | null, headers: string | null, cookies: string | null, };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { invoke as tauriInvoke } from '@tauri-apps/api/core';
|
|||||||
import { error as logError } from '@tauri-apps/plugin-log';
|
import { error as logError } from '@tauri-apps/plugin-log';
|
||||||
import { listen as tauriListen, type Event, type EventCallback, type UnlistenFn } from '@tauri-apps/api/event';
|
import { listen as tauriListen, type Event, type EventCallback, type UnlistenFn } from '@tauri-apps/api/event';
|
||||||
import type { DownloadCategory } from './bindings/DownloadCategory';
|
import type { DownloadCategory } from './bindings/DownloadCategory';
|
||||||
|
import type { DownloadItem } from './bindings/DownloadItem';
|
||||||
import type { DownloadProgressEvent } from './bindings/DownloadProgressEvent';
|
import type { DownloadProgressEvent } from './bindings/DownloadProgressEvent';
|
||||||
import type { DownloadStatus } from './bindings/DownloadStatus';
|
import type { DownloadStatus } from './bindings/DownloadStatus';
|
||||||
import type { ExtensionDownload } from './bindings/ExtensionDownload';
|
import type { ExtensionDownload } from './bindings/ExtensionDownload';
|
||||||
@@ -126,6 +127,7 @@ type EventMap = {
|
|||||||
'download-complete': string;
|
'download-complete': string;
|
||||||
'download-failed': string;
|
'download-failed': string;
|
||||||
'extension-add-download': ExtensionDownload;
|
'extension-add-download': ExtensionDownload;
|
||||||
|
'extension-downloads-queued': DownloadItem[];
|
||||||
'deep-link-add-download': string;
|
'deep-link-add-download': string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user