V2 Tauri integration (#3854)

# Description of Changes

Please provide a summary of the changes, including:

## Add PDF File Association Support for Tauri App

  ### 🎯 **Features Added**
  - PDF file association configuration in Tauri
  - Command line argument detection for opened files
  - Automatic file loading when app is launched via "Open with"
  - Cross-platform support (Windows/macOS)

  ### 🔧 **Technical Changes**
  - Added `fileAssociations` in `tauri.conf.json` for PDF files
  - New `get_opened_file` Tauri command to detect file arguments
  - `fileOpenService` with Tauri fs plugin integration
  - `useOpenedFile` hook for React integration
  - Improved backend health logging during startup (reduced noise)

  ### 🧪 **Testing**
See 
* https://v2.tauri.app/start/prerequisites/
*
[DesktopApplicationDevelopmentGuide.md](DesktopApplicationDevelopmentGuide.md)

  ```bash
  # Test file association during development:
  
  cd frontend
  npm install
  cargo tauri dev --no-watch -- -- "path/to/file.pdf"
  ```

 For production testing:
  1. Build: npm run tauri build
  2. Install the built app
  3. Right-click PDF → "Open with" → Stirling-PDF

  🚀 User Experience

- Users can now double-click PDF files to open them directly in
Stirling-PDF
- Files automatically load in the viewer when opened via file
association
  - Seamless integration with OS file handling

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Connor Yoh <[email protected]>
Co-authored-by: James Brunton <[email protected]>
Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
ConnorYoh
2025-11-05 11:44:59 +00:00
committed by GitHub
co-authored by Connor Yoh James Brunton James Brunton
parent f3eed4428d
commit 4c0c9b28ef
120 changed files with 11005 additions and 1294 deletions
@@ -0,0 +1,147 @@
import { invoke, isTauri } from '@tauri-apps/api/core';
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 {
async getOpenedFile(): Promise<string | null> {
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);
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;
}
}
async clearOpenedFile(): Promise<void> {
try {
console.log('🔍 Calling invoke(clear_opened_file)...');
await invoke('clear_opened_file');
console.log('✅ Successfully cleared opened file');
} catch (error) {
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 (isTauri()) {
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 {
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;
}
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
export const fileOpenService: FileOpenService = isTauri()
? new TauriFileOpenService()
: new WebFileOpenService();