Merge branch 'V2' into V2-CBR-port

This commit is contained in:
Balázs Szücs 2025-11-12 16:59:32 +01:00 committed by GitHub
commit 827283d30a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 591 additions and 111 deletions

View File

@ -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",
]

View File

@ -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"] }

View File

@ -234,6 +234,8 @@ fn monitor_backend_output(mut rx: tauri::async_runtime::Receiver<tauri_plugin_sh
match event {
tauri_plugin_shell::process::CommandEvent::Stdout(output) => {
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<tauri_plugin_sh
}
tauri_plugin_shell::process::CommandEvent::Stderr(output) => {
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

View File

@ -1,49 +1,47 @@
use crate::utils::add_log;
use std::sync::Mutex;
// Store the opened file path globally
static OPENED_FILE: Mutex<Option<String>> = Mutex::new(None);
// Store the opened file paths globally (supports multiple files)
static OPENED_FILES: Mutex<Vec<String>> = 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<Option<String>, 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()));
}
}
pub async fn get_opened_files() -> Result<Vec<String>, String> {
let mut all_files: Vec<String> = Vec::new();
// Fallback to command line arguments (Windows/Linux)
// Get files from command line arguments (Windows/Linux 'Open With Stirling' behaviour)
let args: Vec<String> = std::env::args().collect();
let pdf_files: Vec<String> = args.iter()
.skip(1)
.filter(|arg| std::path::Path::new(arg).exists())
.cloned()
.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()));
}
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(())
}

View File

@ -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};

View File

@ -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());
}
}
}

View File

@ -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",

View File

@ -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]);
}

View File

@ -2,28 +2,28 @@ import { useState, useEffect } from 'react';
import { fileOpenService } from '@app/services/fileOpenService';
export function useOpenedFile() {
const [openedFilePath, setOpenedFilePath] = useState<string | null>(null);
const [openedFilePaths, setOpenedFilePaths] = useState<string[]>([]);
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);
const filePaths = await fileOpenService.getOpenedFiles();
console.log('🔍 fileOpenService.getOpenedFiles() returned:', filePaths);
if (filePath) {
console.log('✅ App opened with file:', filePath);
setOpenedFilePath(filePath);
if (filePaths.length > 0) {
console.log(`✅ App opened with ${filePaths.length} file(s):`, filePaths);
setOpenedFilePaths(filePaths);
// Clear the file from service state after consuming it
await fileOpenService.clearOpenedFile();
// 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 };
}

View File

@ -1,22 +1,22 @@
import { invoke, isTauri } from '@tauri-apps/api/core';
export interface FileOpenService {
getOpenedFile(): Promise<string | null>;
getOpenedFiles(): Promise<string[]>;
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
clearOpenedFile(): Promise<void>;
clearOpenedFiles(): Promise<void>;
onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function
}
class TauriFileOpenService implements FileOpenService {
async getOpenedFile(): Promise<string | null> {
async getOpenedFiles(): Promise<string[]> {
try {
console.log('🔍 Calling invoke(get_opened_file)...');
const result = await invoke<string | null>('get_opened_file');
console.log('🔍 invoke(get_opened_file) returned:', result);
console.log('🔍 Calling invoke(get_opened_files)...');
const result = await invoke<string[]>('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<void> {
async clearOpenedFiles(): Promise<void> {
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<string | null> {
async getOpenedFiles(): Promise<string[]> {
// 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<void> {
async clearOpenedFiles(): Promise<void> {
// In web mode, no file clearing needed
}