Open With Stirling-Pdf

This commit is contained in:
Connor Yoh 2025-07-04 14:33:59 +01:00
parent ed618648e0
commit 780bd663bb
12 changed files with 226 additions and 8 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,10 @@
"main"
],
"permissions": [
"core:default"
"core:default",
{
"identifier": "fs:allow-read-file",
"allow": [{ "path": "**" }]
}
]
}

View File

@ -318,6 +318,23 @@ async fn check_backend_health() -> Result<bool, String> {
// Command to get opened file path (if app was launched with a file)
#[tauri::command]
async fn get_opened_file() -> Result<Option<String>, String> {
// Get command line arguments
let args: Vec<String> = 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<String, String> {
@ -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| {

View File

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

View File

@ -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() {
</div>
</div>
<RainbowThemeProvider>
<HomePage />
<HomePage openedFilePath={openedFilePath} />
</RainbowThemeProvider>
</div>
);

View File

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

View File

@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
import { fileOpenService } from '../services/fileOpenService';
export function useOpenedFile() {
const [openedFilePath, setOpenedFilePath] = useState<string | null>(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 };
}

View File

@ -51,7 +51,11 @@ const toolEndpoints: Record<string, string[]> = {
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)

View File

@ -0,0 +1,53 @@
import { invoke } from '@tauri-apps/api/core';
export interface FileOpenService {
getOpenedFile(): Promise<string | null>;
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
}
class TauriFileOpenService implements FileOpenService {
async getOpenedFile(): Promise<string | null> {
try {
const result = await invoke<string | null>('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<string | null> {
// 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();