From 780bd663bb66fc3ff2270d467d5ce92583c10af9 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Fri, 4 Jul 2025 14:33:59 +0100 Subject: [PATCH] Open With Stirling-Pdf --- frontend/package-lock.json | 16 ++++-- frontend/package.json | 1 + frontend/src-tauri/Cargo.lock | 23 +++++++++ frontend/src-tauri/Cargo.toml | 1 + frontend/src-tauri/capabilities/default.json | 6 ++- frontend/src-tauri/src/lib.rs | 20 +++++++- frontend/src-tauri/tauri.conf.json | 12 ++++- frontend/src/App.tsx | 4 +- frontend/src/hooks/useBackendHealth.ts | 27 ++++++++++ frontend/src/hooks/useOpenedFile.ts | 29 +++++++++++ frontend/src/pages/HomePage.tsx | 42 +++++++++++++++- frontend/src/services/fileOpenService.ts | 53 ++++++++++++++++++++ 12 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 frontend/src/hooks/useOpenedFile.ts create mode 100644 frontend/src/services/fileOpenService.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 05b99c97d..9db3701cb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "@mui/material": "^7.1.0", "@tailwindcss/postcss": "^4.1.8", "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-fs": "^2.4.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", @@ -2020,9 +2021,9 @@ } }, "node_modules/@tauri-apps/api": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz", - "integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.6.0.tgz", + "integrity": "sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg==", "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", @@ -2246,6 +2247,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.0.tgz", + "integrity": "sha512-Sp8AdDcbyXyk6LD6Pmdx44SH3LPeNAvxR2TFfq/8CwqzfO1yOyV+RzT8fov0NNN7d9nvW7O7MtMAptJ42YXA5g==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.6.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 004653ed3..6b9347eab 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "@mui/material": "^7.1.0", "@tailwindcss/postcss": "^4.1.8", "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-fs": "^2.4.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index d0fdec58e..32a032e70 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -100,6 +100,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-fs", "tauri-plugin-log", "tauri-plugin-shell", "tokio", @@ -3871,6 +3872,28 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-fs" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.12", + "toml", + "url", +] + [[package]] name = "tauri-plugin-log" version = "2.4.0" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index f04a9c2be..c09d1455b 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -24,5 +24,6 @@ log = "0.4" tauri = { version = "2.5.0", features = [] } tauri-plugin-log = "2.0.0-rc" tauri-plugin-shell = "2.1.0" +tauri-plugin-fs = "2.0.0" tokio = { version = "1.0", features = ["time"] } reqwest = { version = "0.11", features = ["json"] } diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index c135d7f15..385973667 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -6,6 +6,10 @@ "main" ], "permissions": [ - "core:default" + "core:default", + { + "identifier": "fs:allow-read-file", + "allow": [{ "path": "**" }] + } ] } diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 8b156b2d1..5aaabfd9a 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -318,6 +318,23 @@ async fn check_backend_health() -> Result { +// Command to get opened file path (if app was launched with a file) +#[tauri::command] +async fn get_opened_file() -> Result, String> { + // Get command line arguments + 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: {}", arg)); + return Ok(Some(arg.clone())); + } + } + + Ok(None) +} + // Command to check bundled runtime and JAR #[tauri::command] async fn check_jar_exists(app: tauri::AppHandle) -> Result { @@ -405,6 +422,7 @@ fn cleanup_backend() { pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_fs::init()) .setup(|app| { // Automatically start the backend when Tauri starts @@ -424,7 +442,7 @@ pub fn run() { Ok(()) } ) - .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, check_jar_exists]) + .invoke_handler(tauri::generate_handler![start_backend, check_backend_health, check_jar_exists, get_opened_file]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 1eaa8f912..f36385303 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -32,12 +32,22 @@ "resources": [ "libs/*.jar", "runtime/jre/**/*" + ], + "fileAssociations": [ + { + "ext": ["pdf"], + "name": "PDF Document", + "description": "Open PDF files with Stirling-PDF", + "role": "Editor" + } ] }, "plugins": { "shell": { "open": true + }, + "fs": { + "requireLiteralLeadingDot": false } - } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 39a35dd44..8a67ee1ba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider'; import HomePage from './pages/HomePage'; +import { useOpenedFile } from './hooks/useOpenedFile'; // Import global styles import './styles/tailwind.css'; @@ -9,6 +10,7 @@ import './index.css'; import { BackendHealthIndicator } from './components/BackendHealthIndicator'; export default function App() { + const { openedFilePath, loading: fileLoading } = useOpenedFile(); useEffect(() => { // Only start backend if running in Tauri const initializeBackend = async () => { @@ -39,7 +41,7 @@ export default function App() { - + ); diff --git a/frontend/src/hooks/useBackendHealth.ts b/frontend/src/hooks/useBackendHealth.ts index 6a8bb84dc..506f523fd 100644 --- a/frontend/src/hooks/useBackendHealth.ts +++ b/frontend/src/hooks/useBackendHealth.ts @@ -50,6 +50,15 @@ export const useBackendHealth = (checkInterval: number = 2000) => { }); if (isHealthy) { + // Log success message if this is the first successful check after failures + if (attemptCount > 0) { + const now = new Date(); + const timeSinceStartup = now.getTime() - startupTime.getTime(); + console.log('✅ Backend health check successful:', { + timeSinceStartup: Math.round(timeSinceStartup / 1000) + 's', + attemptsBeforeSuccess: attemptCount, + }); + } setAttemptCount(0); // Reset attempt count on success } } catch (error) { @@ -71,6 +80,24 @@ export const useBackendHealth = (checkInterval: number = 2000) => { errorMessage = isWithinStartupPeriod ? 'Backend starting up...' : 'Health check failed'; } + // Only log errors to console after startup period + if (!isWithinStartupPeriod) { + console.error('Backend health check failed:', { + error: error instanceof Error ? error.message : error, + timeSinceStartup: Math.round(timeSinceStartup / 1000) + 's', + attemptCount, + }); + } else { + // During startup, only log on first few attempts to reduce noise + if (attemptCount <= 3) { + console.log('Backend health check (startup period):', { + message: errorMessage, + timeSinceStartup: Math.round(timeSinceStartup / 1000) + 's', + attempt: attemptCount, + }); + } + } + setHealthState({ isHealthy: false, isChecking: false, diff --git a/frontend/src/hooks/useOpenedFile.ts b/frontend/src/hooks/useOpenedFile.ts new file mode 100644 index 000000000..55343e489 --- /dev/null +++ b/frontend/src/hooks/useOpenedFile.ts @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react'; +import { fileOpenService } from '../services/fileOpenService'; + +export function useOpenedFile() { + const [openedFilePath, setOpenedFilePath] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const checkForOpenedFile = async () => { + try { + const filePath = await fileOpenService.getOpenedFile(); + + if (filePath) { + console.log('✅ App opened with file:', filePath); + setOpenedFilePath(filePath); + } + + } catch (error) { + console.error('❌ Failed to check for opened file:', error); + } finally { + setLoading(false); + } + }; + + checkForOpenedFile(); + }, []); + + return { openedFilePath, loading }; +} \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 7fc1f0905..2e5b1d508 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -51,7 +51,11 @@ const toolEndpoints: Record = { merge: ["merge-pdfs"], }; -export default function HomePage() { +interface HomePageProps { + openedFilePath?: string | null; +} + +export default function HomePage({ openedFilePath }: HomePageProps) { const { t } = useTranslation(); const [searchParams] = useSearchParams(); const theme = useMantineTheme(); @@ -122,6 +126,7 @@ export default function HomePage() { restoreActiveFiles(); }, []); + // Helper function to check if a tool is available const isToolAvailable = (toolKey: string): boolean => { if (endpointsLoading) return true; // Show tools while loading @@ -311,6 +316,41 @@ export default function HomePage() { } }, [handleViewChange, setActiveFiles]); + // Handle opened file from command line arguments + useEffect(() => { + if (openedFilePath) { + const loadOpenedFile = async () => { + try { + console.log('Loading opened file:', openedFilePath); + + // Use the file open service to read the file + const { fileOpenService } = await import('../services/fileOpenService'); + const fileData = await fileOpenService.readFileAsArrayBuffer(openedFilePath); + + if (!fileData) { + throw new Error('Failed to read file data'); + } + + // Create a File object directly from ArrayBuffer + const file = new File([fileData.arrayBuffer], fileData.fileName, { + type: 'application/pdf', + lastModified: Date.now() + }); + + // Add to active files and switch to viewer + addToActiveFiles(file); + setCurrentView('viewer'); + + console.log('Successfully loaded opened file:', fileData.fileName); + } catch (error) { + console.error('Failed to load opened file:', error); + } + }; + + loadOpenedFile(); + } + }, [openedFilePath, addToActiveFiles, setCurrentView]); + const selectedTool = toolRegistry[selectedToolKey]; // For Viewer - convert first active file to expected format (only when needed) diff --git a/frontend/src/services/fileOpenService.ts b/frontend/src/services/fileOpenService.ts new file mode 100644 index 000000000..2ee43a7b7 --- /dev/null +++ b/frontend/src/services/fileOpenService.ts @@ -0,0 +1,53 @@ +import { invoke } from '@tauri-apps/api/core'; + +export interface FileOpenService { + getOpenedFile(): Promise; + readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>; +} + +class TauriFileOpenService implements FileOpenService { + async getOpenedFile(): Promise { + try { + const result = await invoke('get_opened_file'); + return result; + } catch (error) { + console.error('Failed to get opened file:', error); + return null; + } + } + + async readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null> { + try { + const { readFile } = await import('@tauri-apps/plugin-fs'); + + const fileData = await readFile(filePath); + const fileName = filePath.split(/[\\/]/).pop() || 'opened-file.pdf'; + + return { + fileName, + arrayBuffer: fileData.buffer.slice(fileData.byteOffset, fileData.byteOffset + fileData.byteLength) + }; + } catch (error) { + console.error('Failed to read file:', error); + return null; + } + } +} + +class WebFileOpenService implements FileOpenService { + async getOpenedFile(): Promise { + // In web mode, there's no file association support + return null; + } + + async readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null> { + // In web mode, cannot read arbitrary file paths + return null; + } +} + +// Export the appropriate service based on environment +export const fileOpenService: FileOpenService = + typeof window !== 'undefined' && ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) + ? new TauriFileOpenService() + : new WebFileOpenService(); \ No newline at end of file