mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-26 02:27:14 +00:00
feat(desktop): integrate native deep links and tray lifecycle
This commit is contained in:
Generated
-10
@@ -10,7 +10,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.4.9",
|
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
@@ -1656,15 +1655,6 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/plugin-deep-link": {
|
|
||||||
"version": "2.4.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-deep-link/-/plugin-deep-link-2.4.9.tgz",
|
|
||||||
"integrity": "sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA==",
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@tauri-apps/api": "^2.11.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@tauri-apps/plugin-dialog": {
|
"node_modules/@tauri-apps/plugin-dialog": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz",
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-deep-link": "^2.4.9",
|
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
|||||||
Generated
+2
-17
@@ -4405,7 +4405,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"window-vibrancy 0.6.0",
|
"window-vibrancy",
|
||||||
"windows 0.61.3",
|
"windows 0.61.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4444,8 +4444,8 @@ dependencies = [
|
|||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"window-vibrancy 0.7.1",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5734,21 +5734,6 @@ dependencies = [
|
|||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "window-vibrancy"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "010797bd7c40396fbc59d3105089fed0885fe267a0ef4a0a4646df54e28647f6"
|
|
||||||
dependencies = [
|
|
||||||
"objc2",
|
|
||||||
"objc2-app-kit",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
"objc2-foundation",
|
|
||||||
"raw-window-handle",
|
|
||||||
"windows-sys 0.60.2",
|
|
||||||
"windows-version",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.61.3"
|
version = "0.61.3"
|
||||||
|
|||||||
@@ -49,4 +49,4 @@ rusqlite = { version = "0.40.1", features = ["bundled"] }
|
|||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
cocoa = "0.25"
|
cocoa = "0.25"
|
||||||
objc = "0.2.7"
|
objc = "0.2.7"
|
||||||
window-vibrancy = "0.7.1"
|
url = "2"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-start-dragging",
|
"core:window:allow-start-dragging",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"dialog:default",
|
"dialog:default"
|
||||||
"deep-link:default"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use reqwest::{
|
|||||||
Client, StatusCode,
|
Client, StatusCode,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
@@ -26,6 +26,8 @@ pub enum DownloadCmd {
|
|||||||
Start(DownloadPayload),
|
Start(DownloadPayload),
|
||||||
Pause(Uuid),
|
Pause(Uuid),
|
||||||
Cancel(Uuid),
|
Cancel(Uuid),
|
||||||
|
CaptureUrls(Vec<String>),
|
||||||
|
FrontendReady(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -129,6 +131,8 @@ async fn run_coordinator(
|
|||||||
let (worker_tx, mut worker_rx) = mpsc::channel(128);
|
let (worker_tx, mut worker_rx) = mpsc::channel(128);
|
||||||
let mut active = HashMap::<Uuid, ActiveDownload>::new();
|
let mut active = HashMap::<Uuid, ActiveDownload>::new();
|
||||||
let mut active_media = HashMap::<String, watch::Sender<bool>>::new();
|
let mut active_media = HashMap::<String, watch::Sender<bool>>::new();
|
||||||
|
let mut pending_captured_urls = Vec::<String>::new();
|
||||||
|
let mut frontend_ready = false;
|
||||||
let mut next_generation = 0_u64;
|
let mut next_generation = 0_u64;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -169,6 +173,24 @@ async fn run_coordinator(
|
|||||||
let _ = download.control_tx.send(DownloadControl::Cancel).await;
|
let _ = download.control_tx.send(DownloadControl::Cancel).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DownloadCmd::CaptureUrls(urls) => {
|
||||||
|
append_unique_urls(&mut pending_captured_urls, urls);
|
||||||
|
if frontend_ready && !pending_captured_urls.is_empty() {
|
||||||
|
let payload = pending_captured_urls.join("\n");
|
||||||
|
if app_handle.emit("deep-link-add-download", payload).is_ok() {
|
||||||
|
pending_captured_urls.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DownloadCmd::FrontendReady(ready) => {
|
||||||
|
frontend_ready = ready;
|
||||||
|
if ready && !pending_captured_urls.is_empty() {
|
||||||
|
let payload = pending_captured_urls.join("\n");
|
||||||
|
if app_handle.emit("deep-link-add-download", payload).is_ok() {
|
||||||
|
pending_captured_urls.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
event = worker_rx.recv() => {
|
event = worker_rx.recv() => {
|
||||||
@@ -225,6 +247,11 @@ async fn run_coordinator(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn append_unique_urls(target: &mut Vec<String>, urls: Vec<String>) {
|
||||||
|
let mut seen = target.iter().cloned().collect::<HashSet<_>>();
|
||||||
|
target.extend(urls.into_iter().filter(|url| seen.insert(url.clone())));
|
||||||
|
}
|
||||||
|
|
||||||
async fn download_file(
|
async fn download_file(
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
payload: DownloadPayload,
|
payload: DownloadPayload,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use regex::Regex;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
|
|
||||||
#[derive(Serialize, TS)]
|
#[derive(Serialize, TS)]
|
||||||
#[ts(export, export_to = "../../src/bindings/")]
|
#[ts(export, export_to = "../../src/bindings/")]
|
||||||
@@ -385,6 +386,74 @@ fn collect_download_uris(url: &str, mirrors: Option<&str>) -> Vec<String> {
|
|||||||
uris
|
uris
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_DEEP_LINK_PAYLOAD_LEN: usize = 65_536;
|
||||||
|
const MAX_DEEP_LINK_URLS: usize = 200;
|
||||||
|
|
||||||
|
fn parse_firelink_urls(deep_links: impl IntoIterator<Item = url::Url>) -> Vec<String> {
|
||||||
|
let mut captured = Vec::new();
|
||||||
|
|
||||||
|
for deep_link in deep_links {
|
||||||
|
if deep_link.scheme() != "firelink" || deep_link.host_str() != Some("add") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(raw_urls) = deep_link
|
||||||
|
.query_pairs()
|
||||||
|
.find_map(|(key, value)| (key == "url").then(|| value.into_owned()))
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if raw_urls.is_empty() || raw_urls.len() >= MAX_DEEP_LINK_PAYLOAD_LEN {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for raw_url in raw_urls.lines() {
|
||||||
|
let raw_url = raw_url.trim();
|
||||||
|
let Ok(url) = url::Url::parse(raw_url) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !matches!(url.scheme(), "http" | "https" | "ftp" | "sftp") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let url = url.to_string();
|
||||||
|
if !captured.iter().any(|existing| existing == &url) {
|
||||||
|
captured.push(url);
|
||||||
|
if captured.len() == MAX_DEEP_LINK_URLS {
|
||||||
|
return captured;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captured
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_main_window(app_handle: &tauri::AppHandle) {
|
||||||
|
if let Some(window) = app_handle.get_webview_window("main") {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_deep_links(app_handle: tauri::AppHandle, deep_links: Vec<url::Url>) {
|
||||||
|
let urls = parse_firelink_urls(deep_links);
|
||||||
|
if urls.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_main_window(&app_handle);
|
||||||
|
let coordinator = app_handle.state::<AppState>().download_coordinator.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if let Err(error) = coordinator
|
||||||
|
.send(download::DownloadCmd::CaptureUrls(urls))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to dispatch deep link to download coordinator: {error}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fn rpc_call(port: u16, secret: &str, method: &str, params: serde_json::Value) -> Result<serde_json::Value, String> {
|
async fn rpc_call(port: u16, secret: &str, method: &str, params: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||||
let url = format!("http://127.0.0.1:{}/jsonrpc", port);
|
let url = format!("http://127.0.0.1:{}/jsonrpc", port);
|
||||||
let mut payload = serde_json::Map::new();
|
let mut payload = serde_json::Map::new();
|
||||||
@@ -1017,7 +1086,6 @@ fn delete_file(app_handle: tauri::AppHandle, path: String) -> Result<(), String>
|
|||||||
fn toggle_tray_icon(app_handle: tauri::AppHandle, show: bool) -> Result<(), String> {
|
fn toggle_tray_icon(app_handle: tauri::AppHandle, show: bool) -> Result<(), String> {
|
||||||
use tauri::tray::TrayIconBuilder;
|
use tauri::tray::TrayIconBuilder;
|
||||||
use tauri::menu::{Menu, MenuItem};
|
use tauri::menu::{Menu, MenuItem};
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
if show {
|
if show {
|
||||||
if app_handle.tray_by_id("main").is_none() {
|
if app_handle.tray_by_id("main").is_none() {
|
||||||
@@ -1030,18 +1098,30 @@ fn toggle_tray_icon(app_handle: tauri::AppHandle, show: bool) -> Result<(), Stri
|
|||||||
.icon(tray_icon)
|
.icon(tray_icon)
|
||||||
.icon_as_template(true)
|
.icon_as_template(true)
|
||||||
.menu(&menu)
|
.menu(&menu)
|
||||||
|
.show_menu_on_left_click(false)
|
||||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||||
"quit" => {
|
"quit" => {
|
||||||
std::process::exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
"show" => {
|
"show" => {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
restore_main_window(app);
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
|
.on_tray_icon_event(|tray, event| {
|
||||||
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent};
|
||||||
|
|
||||||
|
if matches!(
|
||||||
|
event,
|
||||||
|
TrayIconEvent::Click {
|
||||||
|
button: MouseButton::Left,
|
||||||
|
button_state: MouseButtonState::Up,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
restore_main_window(tray.app_handle());
|
||||||
|
}
|
||||||
|
})
|
||||||
.build(&app_handle)
|
.build(&app_handle)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
}
|
}
|
||||||
@@ -1078,11 +1158,17 @@ fn set_extension_frontend_ready(
|
|||||||
state
|
state
|
||||||
.extension_frontend_ready
|
.extension_frontend_ready
|
||||||
.store(ready, Ordering::Release);
|
.store(ready, Ordering::Release);
|
||||||
|
let coordinator = state.download_coordinator.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
let _ = coordinator
|
||||||
|
.send(download::DownloadCmd::FrontendReady(ready))
|
||||||
|
.await;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::collect_download_uris;
|
use super::{collect_download_uris, parse_firelink_urls};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collects_primary_url_and_unique_mirrors_in_order() {
|
fn collects_primary_url_and_unique_mirrors_in_order() {
|
||||||
@@ -1104,6 +1190,33 @@ mod tests {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_valid_firelink_download_urls() {
|
||||||
|
let deep_link = url::Url::parse(
|
||||||
|
"firelink://add?url=https%3A%2F%2Fexample.com%2Fone.zip%0Aftp%3A%2F%2Fexample.com%2Ftwo.zip",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
parse_firelink_urls([deep_link]),
|
||||||
|
vec![
|
||||||
|
"https://example.com/one.zip",
|
||||||
|
"ftp://example.com/two.zip",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unexpected_deep_links_and_nested_schemes() {
|
||||||
|
let links = [
|
||||||
|
url::Url::parse("firelink://open?url=https%3A%2F%2Fexample.com").unwrap(),
|
||||||
|
url::Url::parse("firelink://add?url=file%3A%2F%2F%2Ftmp%2Fsecret").unwrap(),
|
||||||
|
url::Url::parse("other://add?url=https%3A%2F%2Fexample.com").unwrap(),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert!(parse_firelink_urls(links).is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -1120,10 +1233,7 @@ pub fn run() {
|
|||||||
let aria2_secret = format!("{:x}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
|
let aria2_secret = format!("{:x}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos());
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
restore_main_window(app);
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.manage(Aria2DaemonGuard(std::sync::Mutex::new(None)))
|
.manage(Aria2DaemonGuard(std::sync::Mutex::new(None)))
|
||||||
@@ -1137,21 +1247,20 @@ pub fn run() {
|
|||||||
media_semaphore: Arc::new(tokio::sync::Semaphore::new(3)),
|
media_semaphore: Arc::new(tokio::sync::Semaphore::new(3)),
|
||||||
sleep_preventer: Arc::new(Mutex::new(None)),
|
sleep_preventer: Arc::new(Mutex::new(None)),
|
||||||
});
|
});
|
||||||
|
let deep_link_app = app.handle().clone();
|
||||||
|
app.deep_link().on_open_url(move |event| {
|
||||||
|
dispatch_deep_links(deep_link_app.clone(), event.urls());
|
||||||
|
});
|
||||||
|
match app.deep_link().get_current() {
|
||||||
|
Ok(Some(urls)) => dispatch_deep_links(app.handle().clone(), urls),
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(error) => eprintln!("Failed to read startup deep link: {error}"),
|
||||||
|
}
|
||||||
let db_conn = crate::db::init_db(app.handle()).expect("Failed to init db");
|
let db_conn = crate::db::init_db(app.handle()).expect("Failed to init db");
|
||||||
app.manage(crate::db::DbState { conn: std::sync::Mutex::new(db_conn) });
|
app.manage(crate::db::DbState { conn: std::sync::Mutex::new(db_conn) });
|
||||||
|
|
||||||
crate::scheduler::spawn_scheduler(app.handle().clone());
|
crate::scheduler::spawn_scheduler(app.handle().clone());
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
use tauri::Manager;
|
|
||||||
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
|
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
|
||||||
apply_vibrancy(&window, NSVisualEffectMaterial::Sidebar, None, None)
|
|
||||||
.expect("Unsupported platform! 'apply_vibrancy' is only supported on macOS");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let resource_dir = app.path().resource_dir().unwrap();
|
let resource_dir = app.path().resource_dir().unwrap();
|
||||||
let aria2c_path = resource_dir.join("binaries").join(get_binary_name("aria2c"));
|
let aria2c_path = resource_dir.join("binaries").join(get_binary_name("aria2c"));
|
||||||
|
|
||||||
@@ -1364,6 +1473,16 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
.on_window_event(|window, event| {
|
||||||
|
if window.label() == "main" {
|
||||||
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||||
|
if window.app_handle().tray_by_id("main").is_some() {
|
||||||
|
api.prevent_close();
|
||||||
|
let _ = window.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder,
|
greet, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder,
|
||||||
start_download, start_media_download, pause_download, fetch_metadata, fetch_media_metadata,
|
start_download, start_media_download, pause_download, fetch_metadata, fetch_media_metadata,
|
||||||
|
|||||||
@@ -23,7 +23,11 @@
|
|||||||
"y": 28
|
"y": 28
|
||||||
},
|
},
|
||||||
"hiddenTitle": true,
|
"hiddenTitle": true,
|
||||||
"transparent": true
|
"transparent": true,
|
||||||
|
"windowEffects": {
|
||||||
|
"effects": ["sidebar", "mica"],
|
||||||
|
"state": "active"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
@@ -8,28 +8,9 @@ import { listenEvent as listen, invokeCommand as invoke } from "./ipc";
|
|||||||
import { useDownloadStore, MAIN_QUEUE_ID } from './store/useDownloadStore';
|
import { useDownloadStore, MAIN_QUEUE_ID } from './store/useDownloadStore';
|
||||||
import { useSettingsStore } from "./store/useSettingsStore";
|
import { useSettingsStore } from "./store/useSettingsStore";
|
||||||
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
|
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
|
||||||
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
|
|
||||||
import SchedulerView from "./components/SchedulerView";
|
import SchedulerView from "./components/SchedulerView";
|
||||||
import SpeedLimiterView from "./components/SpeedLimiterView";
|
import SpeedLimiterView from "./components/SpeedLimiterView";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleDeepLinks = (deepLinks: string[]) => {
|
|
||||||
for (const rawDeepLink of deepLinks) {
|
|
||||||
try {
|
|
||||||
const deepLink = new URL(rawDeepLink);
|
|
||||||
if (deepLink.protocol !== 'firelink:' || deepLink.hostname !== 'add') continue;
|
|
||||||
const urls = deepLink.searchParams.get('url') || '';
|
|
||||||
if (urls.length > 0 && urls.length < 65_536) {
|
|
||||||
useDownloadStore.getState().openAddModalWithUrls(urls);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Ignored invalid Firelink deep link:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [filter, setFilter] = useState<SidebarFilter>('all');
|
const [filter, setFilter] = useState<SidebarFilter>('all');
|
||||||
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
const [sidebarWidth, setSidebarWidth] = useState(() => {
|
||||||
@@ -104,19 +85,6 @@ function App() {
|
|||||||
});
|
});
|
||||||
}, [extensionPairingToken]);
|
}, [extensionPairingToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unlisten = onOpenUrl(handleDeepLinks);
|
|
||||||
getCurrent()
|
|
||||||
.then(urls => {
|
|
||||||
if (urls) handleDeepLinks(urls);
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Failed to read startup deep link:', error));
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unlisten.then(dispose => dispose());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previousSpeedLimit.current === globalSpeedLimit) return;
|
if (previousSpeedLimit.current === globalSpeedLimit) return;
|
||||||
previousSpeedLimit.current = globalSpeedLimit;
|
previousSpeedLimit.current = globalSpeedLimit;
|
||||||
@@ -248,7 +216,10 @@ 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);
|
||||||
});
|
});
|
||||||
unlistenExtension
|
const unlistenDeepLink = listen('deep-link-add-download', (event) => {
|
||||||
|
useDownloadStore.getState().openAddModalWithUrls(event.payload);
|
||||||
|
});
|
||||||
|
Promise.all([unlistenExtension, 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));
|
||||||
|
|
||||||
@@ -258,6 +229,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());
|
||||||
|
unlistenDeepLink.then(f => f());
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ type EventMap = {
|
|||||||
'download-complete': string;
|
'download-complete': string;
|
||||||
'download-failed': string;
|
'download-failed': string;
|
||||||
'extension-add-download': ExtensionDownload;
|
'extension-add-download': ExtensionDownload;
|
||||||
|
'deep-link-add-download': string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function listenEvent<K extends keyof EventMap>(
|
export function listenEvent<K extends keyof EventMap>(
|
||||||
|
|||||||
Reference in New Issue
Block a user