mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Fix issues with opening files in desktop app (#4876)
# Description of Changes Locking to just having one instance of the app running unifies the experience across all OSs. Opening new files in Stirling will cause the files to be opened in the existing window rather than spawning a new instance of the app with just that file in the new instance. There's much more to explore here to allow multiple windows open at once, but that can be done all from one instance of the app, and will likely make it easier to allow movement of files etc. across different windows. Also fixes extra newlines in the logs and directly builds to `.app` on Mac because it's frustrating during development to have to repeatedly mount & unmount the `.dmg`.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useBackendInitializer } from '@app/hooks/useBackendInitializer';
|
||||
import { useOpenedFile } from '@app/hooks/useOpenedFile';
|
||||
import { fileOpenService } from '@app/services/fileOpenService';
|
||||
@@ -17,31 +17,78 @@ export function useAppInitialization(): void {
|
||||
// Get file management actions
|
||||
const { addFiles } = useFileManagement();
|
||||
|
||||
// Handle file opened with app (Tauri mode)
|
||||
const { openedFilePath, loading: openedFileLoading } = useOpenedFile();
|
||||
// Handle files opened with app (Tauri mode)
|
||||
const { openedFilePaths, loading: openedFileLoading } = useOpenedFile();
|
||||
|
||||
// Load opened file and add directly to FileContext
|
||||
// Track if we've already loaded the initial files to prevent duplicate loads
|
||||
const initialFilesLoadedRef = useRef(false);
|
||||
|
||||
// Load opened files and add directly to FileContext
|
||||
useEffect(() => {
|
||||
if (openedFilePath && !openedFileLoading) {
|
||||
const loadOpenedFile = async () => {
|
||||
try {
|
||||
const fileData = await fileOpenService.readFileAsArrayBuffer(openedFilePath);
|
||||
if (fileData) {
|
||||
// Create a File object from the ArrayBuffer
|
||||
const file = new File([fileData.arrayBuffer], fileData.fileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
if (openedFilePaths.length > 0 && !openedFileLoading && !initialFilesLoadedRef.current) {
|
||||
initialFilesLoadedRef.current = true;
|
||||
|
||||
// Add directly to FileContext
|
||||
await addFiles([file]);
|
||||
console.log('[Desktop] Opened file added to FileContext:', fileData.fileName);
|
||||
const loadOpenedFiles = async () => {
|
||||
try {
|
||||
const filesArray: File[] = [];
|
||||
|
||||
// Load all files in parallel
|
||||
await Promise.all(
|
||||
openedFilePaths.map(async (filePath) => {
|
||||
try {
|
||||
const fileData = await fileOpenService.readFileAsArrayBuffer(filePath);
|
||||
if (fileData) {
|
||||
const file = new File([fileData.arrayBuffer], fileData.fileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
filesArray.push(file);
|
||||
console.log('[Desktop] Loaded file:', fileData.fileName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Desktop] Failed to load file:', filePath, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (filesArray.length > 0) {
|
||||
// Add all files to FileContext at once
|
||||
await addFiles(filesArray);
|
||||
console.log(`[Desktop] ${filesArray.length} opened file(s) added to FileContext`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Desktop] Failed to load opened file:', error);
|
||||
console.error('[Desktop] Failed to load opened files:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadOpenedFile();
|
||||
loadOpenedFiles();
|
||||
}
|
||||
}, [openedFilePath, openedFileLoading, addFiles]);
|
||||
}, [openedFilePaths, openedFileLoading, addFiles]);
|
||||
|
||||
// Listen for runtime file-opened events (from second instances on Windows/Linux)
|
||||
useEffect(() => {
|
||||
const handleRuntimeFileOpen = async (filePath: string) => {
|
||||
try {
|
||||
console.log('[Desktop] Runtime file-opened event received:', filePath);
|
||||
const fileData = await fileOpenService.readFileAsArrayBuffer(filePath);
|
||||
if (fileData) {
|
||||
// Create a File object from the ArrayBuffer
|
||||
const file = new File([fileData.arrayBuffer], fileData.fileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
|
||||
// Add directly to FileContext
|
||||
await addFiles([file]);
|
||||
console.log('[Desktop] Runtime opened file added to FileContext:', fileData.fileName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Desktop] Failed to load runtime opened file:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Set up event listener and get cleanup function
|
||||
const unlisten = fileOpenService.onFileOpened(handleRuntimeFileOpen);
|
||||
|
||||
// Clean up listener on unmount
|
||||
return unlisten;
|
||||
}, [addFiles]);
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@ import { useState, useEffect } from 'react';
|
||||
import { fileOpenService } from '@app/services/fileOpenService';
|
||||
|
||||
export function useOpenedFile() {
|
||||
const [openedFilePath, setOpenedFilePath] = useState<string | null>(null);
|
||||
const [openedFilePaths, setOpenedFilePaths] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkForOpenedFile = async () => {
|
||||
console.log('🔍 Checking for opened file...');
|
||||
console.log('🔍 Checking for opened file(s)...');
|
||||
try {
|
||||
const filePath = await fileOpenService.getOpenedFile();
|
||||
console.log('🔍 fileOpenService.getOpenedFile() returned:', filePath);
|
||||
|
||||
if (filePath) {
|
||||
console.log('✅ App opened with file:', filePath);
|
||||
setOpenedFilePath(filePath);
|
||||
|
||||
// Clear the file from service state after consuming it
|
||||
await fileOpenService.clearOpenedFile();
|
||||
const filePaths = await fileOpenService.getOpenedFiles();
|
||||
console.log('🔍 fileOpenService.getOpenedFiles() returned:', filePaths);
|
||||
|
||||
if (filePaths.length > 0) {
|
||||
console.log(`✅ App opened with ${filePaths.length} file(s):`, filePaths);
|
||||
setOpenedFilePaths(filePaths);
|
||||
|
||||
// Clear the files from service state after consuming them
|
||||
await fileOpenService.clearOpenedFiles();
|
||||
} else {
|
||||
console.log('ℹ️ No file was opened with the app');
|
||||
console.log('ℹ️ No files were opened with the app');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to check for opened file:', error);
|
||||
console.error('❌ Failed to check for opened files:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export function useOpenedFile() {
|
||||
// Listen for runtime file open events (abstracted through service)
|
||||
const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath: string) => {
|
||||
console.log('📂 Runtime file open event:', filePath);
|
||||
setOpenedFilePath(filePath);
|
||||
setOpenedFilePaths(prev => [...prev, filePath]);
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
@@ -43,5 +43,5 @@ export function useOpenedFile() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { openedFilePath, loading };
|
||||
return { openedFilePaths, loading };
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { invoke, isTauri } from '@tauri-apps/api/core';
|
||||
|
||||
export interface FileOpenService {
|
||||
getOpenedFile(): Promise<string | null>;
|
||||
getOpenedFiles(): Promise<string[]>;
|
||||
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
|
||||
clearOpenedFile(): Promise<void>;
|
||||
clearOpenedFiles(): Promise<void>;
|
||||
onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function
|
||||
}
|
||||
|
||||
class TauriFileOpenService implements FileOpenService {
|
||||
async getOpenedFile(): Promise<string | null> {
|
||||
async getOpenedFiles(): Promise<string[]> {
|
||||
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);
|
||||
console.log('🔍 Calling invoke(get_opened_files)...');
|
||||
const result = await invoke<string[]>('get_opened_files');
|
||||
console.log('🔍 invoke(get_opened_files) returned:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to get opened file:', error);
|
||||
return null;
|
||||
console.error('❌ Failed to get opened files:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,13 @@ class TauriFileOpenService implements FileOpenService {
|
||||
}
|
||||
}
|
||||
|
||||
async clearOpenedFile(): Promise<void> {
|
||||
async clearOpenedFiles(): Promise<void> {
|
||||
try {
|
||||
console.log('🔍 Calling invoke(clear_opened_file)...');
|
||||
await invoke('clear_opened_file');
|
||||
console.log('✅ Successfully cleared opened file');
|
||||
console.log('🔍 Calling invoke(clear_opened_files)...');
|
||||
await invoke('clear_opened_files');
|
||||
console.log('✅ Successfully cleared opened files');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to clear opened file:', error);
|
||||
console.error('❌ Failed to clear opened files:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +67,9 @@ class TauriFileOpenService implements FileOpenService {
|
||||
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);
|
||||
// Listen for unified file open events (all platforms)
|
||||
const unlisten = await listen('file-opened', (event) => {
|
||||
console.log('📂 File open event received:', event.payload);
|
||||
callback(event.payload as string);
|
||||
});
|
||||
|
||||
@@ -83,8 +77,7 @@ class TauriFileOpenService implements FileOpenService {
|
||||
if (!isCleanedUp) {
|
||||
cleanup = () => {
|
||||
try {
|
||||
unlistenMacOS();
|
||||
unlistenFallback();
|
||||
unlisten();
|
||||
console.log('✅ File event listeners cleaned up');
|
||||
} catch (error) {
|
||||
console.error('❌ Error during file event cleanup:', error);
|
||||
@@ -93,8 +86,7 @@ class TauriFileOpenService implements FileOpenService {
|
||||
} else {
|
||||
// Clean up immediately if cleanup was called during setup
|
||||
try {
|
||||
unlistenMacOS();
|
||||
unlistenFallback();
|
||||
unlisten();
|
||||
} catch (error) {
|
||||
console.error('❌ Error during immediate cleanup:', error);
|
||||
}
|
||||
@@ -118,9 +110,9 @@ class TauriFileOpenService implements FileOpenService {
|
||||
}
|
||||
|
||||
class WebFileOpenService implements FileOpenService {
|
||||
async getOpenedFile(): Promise<string | null> {
|
||||
async getOpenedFiles(): Promise<string[]> {
|
||||
// In web mode, there's no file association support
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
async readFileAsArrayBuffer(_filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null> {
|
||||
@@ -128,7 +120,7 @@ class WebFileOpenService implements FileOpenService {
|
||||
return null;
|
||||
}
|
||||
|
||||
async clearOpenedFile(): Promise<void> {
|
||||
async clearOpenedFiles(): Promise<void> {
|
||||
// In web mode, no file clearing needed
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user