diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 84cd44f02..d2abe9651 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -81,6 +81,137 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "atk" version = "0.18.2" @@ -182,6 +313,19 @@ dependencies = [ "objc2 0.6.3", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borsh" version = "1.5.7" @@ -424,6 +568,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -774,6 +927,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "env_filter" version = "0.1.4" @@ -811,6 +991,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -966,6 +1167,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -1352,6 +1566,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -2079,6 +2299,19 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2463,6 +2696,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -2498,6 +2741,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2679,6 +2928,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -2711,6 +2971,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -3657,6 +3931,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stirling-pdf" version = "0.1.0" @@ -3670,6 +3950,7 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-log", "tauri-plugin-shell", + "tauri-plugin-single-instance", "tokio", ] @@ -4056,6 +4337,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.17", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-runtime" version = "2.9.1" @@ -4463,9 +4759,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.108", +] + [[package]] name = "tracing-core" version = "0.1.34" @@ -4515,6 +4823,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -5530,6 +5849,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -5603,3 +5983,43 @@ dependencies = [ "quote", "syn 2.0.108", ] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.108", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.108", + "winnow 0.7.13", +] diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 0ba9b7886..e14bbaaee 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -28,5 +28,6 @@ tauri = { version = "2.9.0", features = [ "devtools"] } tauri-plugin-log = "2.0.0-rc" tauri-plugin-shell = "2.1.0" tauri-plugin-fs = "2.4.4" +tauri-plugin-single-instance = "2.0.1" tokio = { version = "1.0", features = ["time"] } reqwest = { version = "0.11", features = ["json"] } diff --git a/frontend/src-tauri/src/commands/backend.rs b/frontend/src-tauri/src/commands/backend.rs index c465f7cc8..c7bce50f7 100644 --- a/frontend/src-tauri/src/commands/backend.rs +++ b/frontend/src-tauri/src/commands/backend.rs @@ -234,6 +234,8 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver { let output_str = String::from_utf8_lossy(&output); + // Strip exactly one trailing newline to avoid double newlines + let output_str = output_str.strip_suffix('\n').unwrap_or(&output_str); add_log(format!("📤 Backend: {}", output_str)); // Look for startup indicators @@ -250,6 +252,8 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver { let output_str = String::from_utf8_lossy(&output); + // Strip exactly one trailing newline to avoid double newlines + let output_str = output_str.strip_suffix('\n').unwrap_or(&output_str); add_log(format!("đŸ“Ĩ Backend Error: {}", output_str)); // Look for error indicators diff --git a/frontend/src-tauri/src/commands/files.rs b/frontend/src-tauri/src/commands/files.rs index 4b1cd2ad7..2d22ac53e 100644 --- a/frontend/src-tauri/src/commands/files.rs +++ b/frontend/src-tauri/src/commands/files.rs @@ -1,49 +1,47 @@ use crate::utils::add_log; use std::sync::Mutex; -// Store the opened file path globally -static OPENED_FILE: Mutex> = Mutex::new(None); +// Store the opened file paths globally (supports multiple files) +static OPENED_FILES: Mutex> = Mutex::new(Vec::new()); -// Set the opened file path (called by macOS file open events) -#[cfg(target_os = "macos")] -pub fn set_opened_file(file_path: String) { - let mut opened_file = OPENED_FILE.lock().unwrap(); - *opened_file = Some(file_path.clone()); - add_log(format!("📂 File opened via file open event: {}", file_path)); +// Add an opened file path +pub fn add_opened_file(file_path: String) { + let mut opened_files = OPENED_FILES.lock().unwrap(); + opened_files.push(file_path.clone()); + add_log(format!("📂 File stored for later retrieval: {}", file_path)); } -// Command to get opened file path (if app was launched with a file) +// Command to get opened file paths (if app was launched with files) #[tauri::command] -pub async fn get_opened_file() -> Result, String> { - // First check if we have a file from macOS file open events - { - let opened_file = OPENED_FILE.lock().unwrap(); - if let Some(ref file_path) = *opened_file { - add_log(format!("📂 Returning stored opened file: {}", file_path)); - return Ok(Some(file_path.clone())); - } - } - - // Fallback to command line arguments (Windows/Linux) +pub async fn get_opened_files() -> Result, String> { + let mut all_files: Vec = Vec::new(); + + // Get files from command line arguments (Windows/Linux 'Open With Stirling' behaviour) let args: Vec = std::env::args().collect(); - - // Look for a PDF file argument (skip the first arg which is the executable) - for arg in args.iter().skip(1) { - if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() { - add_log(format!("📂 PDF file opened via command line: {}", arg)); - return Ok(Some(arg.clone())); - } + let pdf_files: Vec = args.iter() + .skip(1) + .filter(|arg| std::path::Path::new(arg).exists()) + .cloned() + .collect(); + + all_files.extend(pdf_files); + + // Add any files sent via events or other instances (macOS 'Open With Stirling' behaviour, also Windows/Linux extra files) + { + let opened_files = OPENED_FILES.lock().unwrap(); + all_files.extend(opened_files.clone()); } - - Ok(None) + + add_log(format!("📂 Returning {} opened file(s)", all_files.len())); + Ok(all_files) } -// Command to clear the opened file (after processing) +// Command to clear the opened files (after processing) #[tauri::command] -pub async fn clear_opened_file() -> Result<(), String> { - let mut opened_file = OPENED_FILE.lock().unwrap(); - *opened_file = None; - add_log("📂 Cleared opened file".to_string()); +pub async fn clear_opened_files() -> Result<(), String> { + let mut opened_files = OPENED_FILES.lock().unwrap(); + opened_files.clear(); + add_log("📂 Cleared opened files".to_string()); Ok(()) } diff --git a/frontend/src-tauri/src/commands/mod.rs b/frontend/src-tauri/src/commands/mod.rs index 3f4ed7ecd..f21bf8042 100644 --- a/frontend/src-tauri/src/commands/mod.rs +++ b/frontend/src-tauri/src/commands/mod.rs @@ -4,6 +4,4 @@ pub mod files; pub use backend::{start_backend, cleanup_backend}; pub use health::check_backend_health; -pub use files::{get_opened_file, clear_opened_file}; -#[cfg(target_os = "macos")] -pub use files::set_opened_file; +pub use files::{get_opened_files, clear_opened_files, add_opened_file}; diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 9045e1e6d..9a07845e1 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,13 +1,9 @@ -use tauri::{RunEvent, WindowEvent}; -#[cfg(target_os = "macos")] -use tauri::Emitter; +use tauri::{RunEvent, WindowEvent, Emitter, Manager}; mod utils; mod commands; -use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend}; -#[cfg(target_os = "macos")] -use commands::set_opened_file; +use commands::{start_backend, check_backend_health, get_opened_files, clear_opened_files, cleanup_backend, add_opened_file}; use utils::{add_log, get_tauri_logs}; #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -15,12 +11,35 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { + // This callback runs when a second instance tries to start + add_log(format!("📂 Second instance detected with args: {:?}", args)); + + // Scan args for PDF files (skip first arg which is the executable) + for arg in args.iter().skip(1) { + if std::path::Path::new(arg).exists() { + add_log(format!("📂 Forwarding file to existing instance: {}", arg)); + + // Store file for later retrieval (in case frontend isn't ready yet) + add_opened_file(arg.clone()); + + // Also emit event for immediate handling if frontend is ready + let _ = app.emit("file-opened", arg.clone()); + + // Bring the existing window to front + if let Some(window) = app.get_webview_window("main") { + let _ = window.set_focus(); + let _ = window.unminimize(); + } + } + } + })) .setup(|_app| { add_log("🚀 Tauri app setup started".to_string()); add_log("🔍 DEBUG: Setup completed".to_string()); Ok(()) }) - .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file, clear_opened_file, get_tauri_logs]) + .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_files, clear_opened_files, get_tauri_logs]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { @@ -45,8 +64,9 @@ pub fn run() { let file_path = url_str.strip_prefix("file://").unwrap_or(url_str); if file_path.ends_with(".pdf") { add_log(format!("📂 Processing opened PDF: {}", file_path)); - set_opened_file(file_path.to_string()); - let _ = app_handle.emit("macos://open-file", file_path.to_string()); + add_opened_file(file_path.to_string()); + // Use unified event name for consistency across platforms + let _ = app_handle.emit("file-opened", file_path.to_string()); } } } @@ -58,4 +78,4 @@ pub fn run() { } } }); -} \ No newline at end of file +} diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 45bb852da..d845a2dc9 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -22,7 +22,7 @@ }, "bundle": { "active": true, - "targets": ["deb", "rpm", "dmg", "msi"], + "targets": ["deb", "rpm", "dmg", "app", "msi"], "icon": [ "icons/icon.png", "icons/icon.icns", diff --git a/frontend/src/desktop/hooks/useAppInitialization.ts b/frontend/src/desktop/hooks/useAppInitialization.ts index c64ea1180..a34547bcd 100644 --- a/frontend/src/desktop/hooks/useAppInitialization.ts +++ b/frontend/src/desktop/hooks/useAppInitialization.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useBackendInitializer } from '@app/hooks/useBackendInitializer'; import { useOpenedFile } from '@app/hooks/useOpenedFile'; import { fileOpenService } from '@app/services/fileOpenService'; @@ -17,31 +17,78 @@ export function useAppInitialization(): void { // Get file management actions const { addFiles } = useFileManagement(); - // Handle file opened with app (Tauri mode) - const { openedFilePath, loading: openedFileLoading } = useOpenedFile(); + // Handle files opened with app (Tauri mode) + const { openedFilePaths, loading: openedFileLoading } = useOpenedFile(); - // Load opened file and add directly to FileContext + // Track if we've already loaded the initial files to prevent duplicate loads + const initialFilesLoadedRef = useRef(false); + + // Load opened files and add directly to FileContext useEffect(() => { - if (openedFilePath && !openedFileLoading) { - const loadOpenedFile = async () => { - try { - const fileData = await fileOpenService.readFileAsArrayBuffer(openedFilePath); - if (fileData) { - // Create a File object from the ArrayBuffer - const file = new File([fileData.arrayBuffer], fileData.fileName, { - type: 'application/pdf' - }); + if (openedFilePaths.length > 0 && !openedFileLoading && !initialFilesLoadedRef.current) { + initialFilesLoadedRef.current = true; - // Add directly to FileContext - await addFiles([file]); - console.log('[Desktop] Opened file added to FileContext:', fileData.fileName); + const loadOpenedFiles = async () => { + try { + const filesArray: File[] = []; + + // Load all files in parallel + await Promise.all( + openedFilePaths.map(async (filePath) => { + try { + const fileData = await fileOpenService.readFileAsArrayBuffer(filePath); + if (fileData) { + const file = new File([fileData.arrayBuffer], fileData.fileName, { + type: 'application/pdf' + }); + filesArray.push(file); + console.log('[Desktop] Loaded file:', fileData.fileName); + } + } catch (error) { + console.error('[Desktop] Failed to load file:', filePath, error); + } + }) + ); + + if (filesArray.length > 0) { + // Add all files to FileContext at once + await addFiles(filesArray); + console.log(`[Desktop] ${filesArray.length} opened file(s) added to FileContext`); } } catch (error) { - console.error('[Desktop] Failed to load opened file:', error); + console.error('[Desktop] Failed to load opened files:', error); } }; - loadOpenedFile(); + loadOpenedFiles(); } - }, [openedFilePath, openedFileLoading, addFiles]); + }, [openedFilePaths, openedFileLoading, addFiles]); + + // Listen for runtime file-opened events (from second instances on Windows/Linux) + useEffect(() => { + const handleRuntimeFileOpen = async (filePath: string) => { + try { + console.log('[Desktop] Runtime file-opened event received:', filePath); + const fileData = await fileOpenService.readFileAsArrayBuffer(filePath); + if (fileData) { + // Create a File object from the ArrayBuffer + const file = new File([fileData.arrayBuffer], fileData.fileName, { + type: 'application/pdf' + }); + + // Add directly to FileContext + await addFiles([file]); + console.log('[Desktop] Runtime opened file added to FileContext:', fileData.fileName); + } + } catch (error) { + console.error('[Desktop] Failed to load runtime opened file:', error); + } + }; + + // Set up event listener and get cleanup function + const unlisten = fileOpenService.onFileOpened(handleRuntimeFileOpen); + + // Clean up listener on unmount + return unlisten; + }, [addFiles]); } diff --git a/frontend/src/desktop/hooks/useOpenedFile.ts b/frontend/src/desktop/hooks/useOpenedFile.ts index 17d6a7870..48565010f 100644 --- a/frontend/src/desktop/hooks/useOpenedFile.ts +++ b/frontend/src/desktop/hooks/useOpenedFile.ts @@ -2,28 +2,28 @@ import { useState, useEffect } from 'react'; import { fileOpenService } from '@app/services/fileOpenService'; export function useOpenedFile() { - const [openedFilePath, setOpenedFilePath] = useState(null); + const [openedFilePaths, setOpenedFilePaths] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const checkForOpenedFile = async () => { - console.log('🔍 Checking for opened file...'); + console.log('🔍 Checking for opened file(s)...'); try { - const filePath = await fileOpenService.getOpenedFile(); - console.log('🔍 fileOpenService.getOpenedFile() returned:', filePath); - - if (filePath) { - console.log('✅ App opened with file:', filePath); - setOpenedFilePath(filePath); - - // Clear the file from service state after consuming it - await fileOpenService.clearOpenedFile(); + const filePaths = await fileOpenService.getOpenedFiles(); + console.log('🔍 fileOpenService.getOpenedFiles() returned:', filePaths); + + if (filePaths.length > 0) { + console.log(`✅ App opened with ${filePaths.length} file(s):`, filePaths); + setOpenedFilePaths(filePaths); + + // Clear the files from service state after consuming them + await fileOpenService.clearOpenedFiles(); } else { - console.log('â„šī¸ No file was opened with the app'); + console.log('â„šī¸ No files were opened with the app'); } } catch (error) { - console.error('❌ Failed to check for opened file:', error); + console.error('❌ Failed to check for opened files:', error); } finally { setLoading(false); } @@ -34,7 +34,7 @@ export function useOpenedFile() { // Listen for runtime file open events (abstracted through service) const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath: string) => { console.log('📂 Runtime file open event:', filePath); - setOpenedFilePath(filePath); + setOpenedFilePaths(prev => [...prev, filePath]); }); // Cleanup function @@ -43,5 +43,5 @@ export function useOpenedFile() { }; }, []); - return { openedFilePath, loading }; + return { openedFilePaths, loading }; } diff --git a/frontend/src/desktop/services/fileOpenService.ts b/frontend/src/desktop/services/fileOpenService.ts index 524776d32..67813df38 100644 --- a/frontend/src/desktop/services/fileOpenService.ts +++ b/frontend/src/desktop/services/fileOpenService.ts @@ -1,22 +1,22 @@ import { invoke, isTauri } from '@tauri-apps/api/core'; export interface FileOpenService { - getOpenedFile(): Promise; + getOpenedFiles(): Promise; readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>; - clearOpenedFile(): Promise; + clearOpenedFiles(): Promise; onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function } class TauriFileOpenService implements FileOpenService { - async getOpenedFile(): Promise { + async getOpenedFiles(): Promise { try { - console.log('🔍 Calling invoke(get_opened_file)...'); - const result = await invoke('get_opened_file'); - console.log('🔍 invoke(get_opened_file) returned:', result); + console.log('🔍 Calling invoke(get_opened_files)...'); + const result = await invoke('get_opened_files'); + console.log('🔍 invoke(get_opened_files) returned:', result); return result; } catch (error) { - console.error('❌ Failed to get opened file:', error); - return null; + console.error('❌ Failed to get opened files:', error); + return []; } } @@ -37,13 +37,13 @@ class TauriFileOpenService implements FileOpenService { } } - async clearOpenedFile(): Promise { + async clearOpenedFiles(): Promise { try { - console.log('🔍 Calling invoke(clear_opened_file)...'); - await invoke('clear_opened_file'); - console.log('✅ Successfully cleared opened file'); + console.log('🔍 Calling invoke(clear_opened_files)...'); + await invoke('clear_opened_files'); + console.log('✅ Successfully cleared opened files'); } catch (error) { - console.error('❌ Failed to clear opened file:', error); + console.error('❌ Failed to clear opened files:', error); } } @@ -67,15 +67,9 @@ class TauriFileOpenService implements FileOpenService { return; } - // Listen for macOS native file open events - const unlistenMacOS = await listen('macos://open-file', (event) => { - console.log('📂 macOS native file open event:', event.payload); - callback(event.payload as string); - }); - - // Listen for fallback file open events - const unlistenFallback = await listen('file-opened', (event) => { - console.log('📂 Fallback file open event:', event.payload); + // Listen for unified file open events (all platforms) + const unlisten = await listen('file-opened', (event) => { + console.log('📂 File open event received:', event.payload); callback(event.payload as string); }); @@ -83,8 +77,7 @@ class TauriFileOpenService implements FileOpenService { if (!isCleanedUp) { cleanup = () => { try { - unlistenMacOS(); - unlistenFallback(); + unlisten(); console.log('✅ File event listeners cleaned up'); } catch (error) { console.error('❌ Error during file event cleanup:', error); @@ -93,8 +86,7 @@ class TauriFileOpenService implements FileOpenService { } else { // Clean up immediately if cleanup was called during setup try { - unlistenMacOS(); - unlistenFallback(); + unlisten(); } catch (error) { console.error('❌ Error during immediate cleanup:', error); } @@ -118,9 +110,9 @@ class TauriFileOpenService implements FileOpenService { } class WebFileOpenService implements FileOpenService { - async getOpenedFile(): Promise { + async getOpenedFiles(): Promise { // In web mode, there's no file association support - return null; + return []; } async readFileAsArrayBuffer(_filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null> { @@ -128,7 +120,7 @@ class WebFileOpenService implements FileOpenService { return null; } - async clearOpenedFile(): Promise { + async clearOpenedFiles(): Promise { // In web mode, no file clearing needed }