mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-30 17:15:21 +00:00
Open With Stirling-Pdf
This commit is contained in:
parent
ed618648e0
commit
780bd663bb
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@ -17,6 +17,7 @@
|
|||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.0",
|
||||||
"@tailwindcss/postcss": "^4.1.8",
|
"@tailwindcss/postcss": "^4.1.8",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
|
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
@ -2020,9 +2021,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/api": {
|
"node_modules/@tauri-apps/api": {
|
||||||
"version": "2.5.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.6.0.tgz",
|
||||||
"integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==",
|
"integrity": "sha512-hRNcdercfgpzgFrMXWwNDBN0B7vNzOzRepy6ZAmhxi5mDLVPNrTpo9MGg2tN/F7JRugj4d2aF7E1rtPXAHaetg==",
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -2246,6 +2247,15 @@
|
|||||||
"node": ">= 10"
|
"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": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.0",
|
||||||
"@tailwindcss/postcss": "^4.1.8",
|
"@tailwindcss/postcss": "^4.1.8",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
|
"@tauri-apps/plugin-fs": "^2.4.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
|
23
frontend/src-tauri/Cargo.lock
generated
23
frontend/src-tauri/Cargo.lock
generated
@ -100,6 +100,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -3871,6 +3872,28 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-log"
|
name = "tauri-plugin-log"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
|
@ -24,5 +24,6 @@ log = "0.4"
|
|||||||
tauri = { version = "2.5.0", features = [] }
|
tauri = { version = "2.5.0", features = [] }
|
||||||
tauri-plugin-log = "2.0.0-rc"
|
tauri-plugin-log = "2.0.0-rc"
|
||||||
tauri-plugin-shell = "2.1.0"
|
tauri-plugin-shell = "2.1.0"
|
||||||
|
tauri-plugin-fs = "2.0.0"
|
||||||
tokio = { version = "1.0", features = ["time"] }
|
tokio = { version = "1.0", features = ["time"] }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
@ -6,6 +6,10 @@
|
|||||||
"main"
|
"main"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default"
|
"core:default",
|
||||||
|
{
|
||||||
|
"identifier": "fs:allow-read-file",
|
||||||
|
"allow": [{ "path": "**" }]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -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
|
// Command to check bundled runtime and JAR
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn check_jar_exists(app: tauri::AppHandle) -> Result<String, String> {
|
async fn check_jar_exists(app: tauri::AppHandle) -> Result<String, String> {
|
||||||
@ -405,6 +422,7 @@ fn cleanup_backend() {
|
|||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_fs::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
|
||||||
// Automatically start the backend when Tauri starts
|
// Automatically start the backend when Tauri starts
|
||||||
@ -424,7 +442,7 @@ pub fn run() {
|
|||||||
Ok(())
|
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!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while building tauri application")
|
.expect("error while building tauri application")
|
||||||
.run(|app_handle, event| {
|
.run(|app_handle, event| {
|
||||||
|
@ -32,12 +32,22 @@
|
|||||||
"resources": [
|
"resources": [
|
||||||
"libs/*.jar",
|
"libs/*.jar",
|
||||||
"runtime/jre/**/*"
|
"runtime/jre/**/*"
|
||||||
|
],
|
||||||
|
"fileAssociations": [
|
||||||
|
{
|
||||||
|
"ext": ["pdf"],
|
||||||
|
"name": "PDF Document",
|
||||||
|
"description": "Open PDF files with Stirling-PDF",
|
||||||
|
"role": "Editor"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"requireLiteralLeadingDot": false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
|
import { RainbowThemeProvider } from './components/shared/RainbowThemeProvider';
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
|
import { useOpenedFile } from './hooks/useOpenedFile';
|
||||||
|
|
||||||
// Import global styles
|
// Import global styles
|
||||||
import './styles/tailwind.css';
|
import './styles/tailwind.css';
|
||||||
@ -9,6 +10,7 @@ import './index.css';
|
|||||||
import { BackendHealthIndicator } from './components/BackendHealthIndicator';
|
import { BackendHealthIndicator } from './components/BackendHealthIndicator';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const { openedFilePath, loading: fileLoading } = useOpenedFile();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only start backend if running in Tauri
|
// Only start backend if running in Tauri
|
||||||
const initializeBackend = async () => {
|
const initializeBackend = async () => {
|
||||||
@ -39,7 +41,7 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RainbowThemeProvider>
|
<RainbowThemeProvider>
|
||||||
<HomePage />
|
<HomePage openedFilePath={openedFilePath} />
|
||||||
</RainbowThemeProvider>
|
</RainbowThemeProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -50,6 +50,15 @@ export const useBackendHealth = (checkInterval: number = 2000) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isHealthy) {
|
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
|
setAttemptCount(0); // Reset attempt count on success
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -71,6 +80,24 @@ export const useBackendHealth = (checkInterval: number = 2000) => {
|
|||||||
errorMessage = isWithinStartupPeriod ? 'Backend starting up...' : 'Health check failed';
|
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({
|
setHealthState({
|
||||||
isHealthy: false,
|
isHealthy: false,
|
||||||
isChecking: false,
|
isChecking: false,
|
||||||
|
29
frontend/src/hooks/useOpenedFile.ts
Normal file
29
frontend/src/hooks/useOpenedFile.ts
Normal 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 };
|
||||||
|
}
|
@ -51,7 +51,11 @@ const toolEndpoints: Record<string, string[]> = {
|
|||||||
merge: ["merge-pdfs"],
|
merge: ["merge-pdfs"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HomePage() {
|
interface HomePageProps {
|
||||||
|
openedFilePath?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomePage({ openedFilePath }: HomePageProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
@ -122,6 +126,7 @@ export default function HomePage() {
|
|||||||
restoreActiveFiles();
|
restoreActiveFiles();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Helper function to check if a tool is available
|
// Helper function to check if a tool is available
|
||||||
const isToolAvailable = (toolKey: string): boolean => {
|
const isToolAvailable = (toolKey: string): boolean => {
|
||||||
if (endpointsLoading) return true; // Show tools while loading
|
if (endpointsLoading) return true; // Show tools while loading
|
||||||
@ -311,6 +316,41 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
}, [handleViewChange, setActiveFiles]);
|
}, [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];
|
const selectedTool = toolRegistry[selectedToolKey];
|
||||||
|
|
||||||
// For Viewer - convert first active file to expected format (only when needed)
|
// For Viewer - convert first active file to expected format (only when needed)
|
||||||
|
53
frontend/src/services/fileOpenService.ts
Normal file
53
frontend/src/services/fileOpenService.ts
Normal 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();
|
Loading…
x
Reference in New Issue
Block a user