mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-02 13:48:15 +02:00
Mac specific file handler
This commit is contained in:
parent
4d05a1be41
commit
e0ec0fbf4a
87
frontend/src-tauri/Cargo.lock
generated
87
frontend/src-tauri/Cargo.lock
generated
@ -94,7 +94,10 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"log",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -195,6 +198,12 @@ dependencies = [
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@ -454,6 +463,36 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics 0.22.3",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@ -506,6 +545,19 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.22.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.24.0"
|
||||
@ -514,11 +566,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"core-graphics-types 0.2.0",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.2.0"
|
||||
@ -1978,6 +2041,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.11.0"
|
||||
@ -2165,6 +2237,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@ -3509,7 +3590,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
@ -3687,7 +3768,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
"dlopen2",
|
||||
|
@ -27,3 +27,9 @@ tauri-plugin-shell = "2.1.0"
|
||||
tauri-plugin-fs = "2.0.0"
|
||||
tokio = { version = "1.0", features = ["time"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
# macOS-specific dependencies for native file opening
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2"
|
||||
cocoa = "0.24"
|
||||
once_cell = "1.19"
|
||||
|
116
frontend/src-tauri/src/file_handler.rs
Normal file
116
frontend/src-tauri/src/file_handler.rs
Normal file
@ -0,0 +1,116 @@
|
||||
/// Multi-platform file opening handler
|
||||
///
|
||||
/// This module provides unified file opening support across platforms:
|
||||
/// - macOS: Uses native NSApplication delegate (proper Apple Events)
|
||||
/// - Windows/Linux: Uses command line arguments (fallback approach)
|
||||
/// - All platforms: Runtime event handling via Tauri events
|
||||
|
||||
use crate::utils::add_log;
|
||||
use crate::commands::set_opened_file;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_native;
|
||||
|
||||
/// Initialize file handling for the current platform
|
||||
pub fn initialize_file_handler<R: Runtime>(app: &AppHandle<R>) {
|
||||
add_log("🔧 Initializing file handler...".to_string());
|
||||
|
||||
// Platform-specific initialization
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
add_log("🍎 Using macOS native file handler".to_string());
|
||||
macos_native::register_open_file_handler(app);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
add_log("🖥️ Using command line argument file handler".to_string());
|
||||
let _ = app; // Suppress unused variable warning
|
||||
}
|
||||
|
||||
// Universal: Check command line arguments (works on all platforms)
|
||||
check_command_line_args();
|
||||
}
|
||||
|
||||
/// Check command line arguments for file paths (universal fallback)
|
||||
fn check_command_line_args() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
add_log(format!("🔍 DEBUG: All command line args: {:?}", args));
|
||||
|
||||
// Check command line arguments for file opening
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
add_log(format!("🔍 DEBUG: Arg {}: {}", i, arg));
|
||||
if i > 0 && arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 File argument detected: {}", arg));
|
||||
set_opened_file(arg.clone());
|
||||
break; // Only handle the first PDF file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle runtime file open events (for future single-instance support)
|
||||
pub fn handle_runtime_file_open(file_path: String) {
|
||||
if file_path.ends_with(".pdf") && std::path::Path::new(&file_path).exists() {
|
||||
add_log(format!("📂 Runtime file open: {}", file_path));
|
||||
set_opened_file(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_native {
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use objc::runtime::{Class, Object, Sel};
|
||||
use cocoa::appkit::NSApplication;
|
||||
use cocoa::base::{id, nil};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Runtime, Manager};
|
||||
|
||||
use crate::utils::add_log;
|
||||
use crate::commands::set_opened_file;
|
||||
|
||||
static APP_HANDLE: Lazy<Mutex<Option<AppHandle>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
extern "C" fn open_file(_self: &Object, _cmd: Sel, _sender: id, filename: id) -> bool {
|
||||
unsafe {
|
||||
let cstr = {
|
||||
let bytes: *const std::os::raw::c_char = msg_send![filename, UTF8String];
|
||||
std::ffi::CStr::from_ptr(bytes)
|
||||
};
|
||||
if let Ok(path) = cstr.to_str() {
|
||||
add_log(format!("📂 macOS native file open event: {}", path));
|
||||
if path.ends_with(".pdf") {
|
||||
set_opened_file(path.to_string());
|
||||
|
||||
if let Some(app) = APP_HANDLE.lock().unwrap().as_ref() {
|
||||
let _ = app.emit("macos://open-file", path.to_string());
|
||||
add_log(format!("✅ Emitted file open event to frontend: {}", path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn register_open_file_handler<R: Runtime>(app: &AppHandle<R>) {
|
||||
add_log("🔧 Registering macOS native file handler...".to_string());
|
||||
|
||||
*APP_HANDLE.lock().unwrap() = Some(app.clone());
|
||||
|
||||
unsafe {
|
||||
let delegate_class = Class::get("AppDelegate").unwrap_or_else(|| {
|
||||
let superclass = class!(NSObject);
|
||||
let mut decl = objc::declare::ClassDecl::new("AppDelegate", superclass).unwrap();
|
||||
decl.add_method(sel!(application:openFile:), open_file as extern "C" fn(&Object, Sel, id, id) -> bool);
|
||||
decl.register()
|
||||
});
|
||||
|
||||
let delegate: id = msg_send![delegate_class, new];
|
||||
let ns_app = NSApplication::sharedApplication(nil);
|
||||
ns_app.setDelegate_(delegate);
|
||||
}
|
||||
|
||||
add_log("✅ macOS native file handler registered successfully".to_string());
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
use tauri::{RunEvent, WindowEvent, Manager};
|
||||
use tauri::{RunEvent, WindowEvent};
|
||||
|
||||
mod utils;
|
||||
mod commands;
|
||||
mod file_handler;
|
||||
|
||||
use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend, set_opened_file};
|
||||
use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend};
|
||||
use utils::{add_log, get_tauri_logs};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
@ -11,24 +12,13 @@ pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.setup(|_| {
|
||||
.setup(|app| {
|
||||
add_log("🚀 Tauri app setup started".to_string());
|
||||
|
||||
// Log all command line arguments for debugging
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
add_log(format!("🔍 DEBUG: All command line args: {:?}", args));
|
||||
// Initialize platform-specific file handler
|
||||
file_handler::initialize_file_handler(&app.handle());
|
||||
|
||||
// Check command line arguments at startup for macOS file opening
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
add_log(format!("🔍 DEBUG: Arg {}: {}", i, arg));
|
||||
if i > 0 && arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 File argument detected at startup: {}", arg));
|
||||
set_opened_file(arg.clone());
|
||||
break; // Only handle the first PDF file
|
||||
}
|
||||
}
|
||||
|
||||
add_log("🔍 DEBUG: Setup completed, checking for opened file...".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])
|
||||
@ -47,24 +37,10 @@ pub fn run() {
|
||||
cleanup_backend();
|
||||
// Allow the window to close
|
||||
}
|
||||
// Handle file open events (macOS specific)
|
||||
#[cfg(target_os = "macos")]
|
||||
RunEvent::OpenUrl { url } => {
|
||||
add_log(format!("🔍 DEBUG: OpenUrl event received: {}", url));
|
||||
// Handle URL-based file opening
|
||||
if url.starts_with("file://") {
|
||||
let file_path = url.strip_prefix("file://").unwrap_or(&url);
|
||||
if file_path.ends_with(".pdf") {
|
||||
add_log(format!("📂 File opened via URL event: {}", file_path));
|
||||
set_opened_file(file_path.to_string());
|
||||
|
||||
// Emit event to frontend
|
||||
app_handle.emit_all("file-opened", file_path).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
add_log(format!("🔍 DEBUG: Unhandled event: {:?}", event));
|
||||
// Only log unhandled events in debug mode to reduce noise
|
||||
// #[cfg(debug_assertions)]
|
||||
// add_log(format!("🔍 DEBUG: Unhandled event: {:?}", event));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -16,7 +16,7 @@ export function useOpenedFile() {
|
||||
console.log('✅ App opened with file:', filePath);
|
||||
setOpenedFilePath(filePath);
|
||||
|
||||
// Clear the file from Tauri state after consuming it
|
||||
// Clear the file from service state after consuming it
|
||||
await fileOpenService.clearOpenedFile();
|
||||
} else {
|
||||
console.log('ℹ️ No file was opened with the app');
|
||||
@ -30,6 +30,17 @@ export function useOpenedFile() {
|
||||
};
|
||||
|
||||
checkForOpenedFile();
|
||||
|
||||
// Listen for runtime file open events (abstracted through service)
|
||||
const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath) => {
|
||||
console.log('📂 Runtime file open event:', filePath);
|
||||
setOpenedFilePath(filePath);
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
unlistenRuntimeEvents();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { openedFilePath, loading };
|
||||
|
@ -4,6 +4,7 @@ export interface FileOpenService {
|
||||
getOpenedFile(): Promise<string | null>;
|
||||
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
|
||||
clearOpenedFile(): Promise<void>;
|
||||
onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function
|
||||
}
|
||||
|
||||
class TauriFileOpenService implements FileOpenService {
|
||||
@ -45,6 +46,75 @@ class TauriFileOpenService implements FileOpenService {
|
||||
console.error('❌ Failed to clear opened file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onFileOpened(callback: (filePath: string) => void): () => void {
|
||||
let cleanup: (() => void) | null = null;
|
||||
let isCleanedUp = false;
|
||||
|
||||
const setupEventListeners = async () => {
|
||||
try {
|
||||
// Check if already cleaned up before async setup completes
|
||||
if (isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only import if in Tauri environment
|
||||
if (typeof window !== 'undefined' && ('__TAURI__' in window || '__TAURI_INTERNALS__' in window)) {
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
|
||||
// Check again after async import
|
||||
if (isCleanedUp) {
|
||||
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);
|
||||
callback(event.payload as string);
|
||||
});
|
||||
|
||||
// Set up cleanup function only if not already cleaned up
|
||||
if (!isCleanedUp) {
|
||||
cleanup = () => {
|
||||
try {
|
||||
unlistenMacOS();
|
||||
unlistenFallback();
|
||||
console.log('✅ File event listeners cleaned up');
|
||||
} catch (error) {
|
||||
console.error('❌ Error during file event cleanup:', error);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Clean up immediately if cleanup was called during setup
|
||||
try {
|
||||
unlistenMacOS();
|
||||
unlistenFallback();
|
||||
} catch (error) {
|
||||
console.error('❌ Error during immediate cleanup:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to setup file event listeners:', error);
|
||||
}
|
||||
};
|
||||
|
||||
setupEventListeners();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
isCleanedUp = true;
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class WebFileOpenService implements FileOpenService {
|
||||
@ -61,6 +131,14 @@ class WebFileOpenService implements FileOpenService {
|
||||
async clearOpenedFile(): Promise<void> {
|
||||
// In web mode, no file clearing needed
|
||||
}
|
||||
|
||||
onFileOpened(callback: (filePath: string) => void): () => void {
|
||||
// In web mode, no file events - return no-op cleanup function
|
||||
console.log('ℹ️ Web mode: File event listeners not supported');
|
||||
return () => {
|
||||
// No-op cleanup for web mode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export the appropriate service based on environment
|
||||
|
Loading…
Reference in New Issue
Block a user