mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
# Summary - Adds desktop file tracking: local paths are preserved and save buttons now work as expcted (doing Save/Save As as appropriate) - Adds logic to track whether files are 'dirty' (they've been modified by some tool, and not saved to disk yet). - Improves file state UX (dirty vs saved) and close warnings - Web behaviour should be unaffected by these changes ## Indicators Files now have indicators in desktop mode to tell you their state. ### File up-to-date with disk <img width="318" height="393" alt="image" src="https://github.com/user-attachments/assets/06325f9a-afd7-4c2f-8a5b-6d11e3093115" /> ### File modified by a tool but not saved to disk yet <img width="357" height="385" alt="image" src="https://github.com/user-attachments/assets/1a7716d9-c6f7-4d13-be0d-c1de6493954b" /> ### File not tracked on disk <img width="312" height="379" alt="image" src="https://github.com/user-attachments/assets/9cffe300-bd9a-4e19-97c7-9b98bebefacc" /> # Limitations - It's a bit weird that we still have files stored in indexeddb in the app, which are still loadable. We might want to change this behaviour in the future - Viewer's Save doesn't persist to disk. I've left that out here because it'd need a lot of testing to make sure the logic's right with making sure you can leave the Viewer with applying the changes to the PDF _without_ saving to disk - There's no current way to do Save As on a file that has already been persisted to disk - it's only ever Save. Similarly, there's no way to duplicate a file. --------- Co-authored-by: James Brunton <[email protected]> Co-authored-by: James Brunton <[email protected]>
140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import { invoke, isTauri } from '@tauri-apps/api/core';
|
||
|
||
export interface FileOpenService {
|
||
getOpenedFiles(): Promise<string[]>;
|
||
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
|
||
clearOpenedFiles(): Promise<void>;
|
||
onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function
|
||
}
|
||
|
||
class TauriFileOpenService implements FileOpenService {
|
||
async getOpenedFiles(): Promise<string[]> {
|
||
try {
|
||
console.log('🔍 Calling invoke(pop_opened_files)...');
|
||
const result = await invoke<string[]>('pop_opened_files');
|
||
console.log('🔍 invoke(pop_opened_files) returned:', result);
|
||
return result;
|
||
} catch (error) {
|
||
console.error('❌ Failed to get opened files:', error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
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 clearOpenedFiles(): Promise<void> {
|
||
try {
|
||
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 files:', 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 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);
|
||
});
|
||
|
||
// Set up cleanup function only if not already cleaned up
|
||
if (!isCleanedUp) {
|
||
cleanup = () => {
|
||
try {
|
||
unlisten();
|
||
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 {
|
||
unlisten();
|
||
} 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 getOpenedFiles(): Promise<string[]> {
|
||
// In web mode, there's no file association support
|
||
return [];
|
||
}
|
||
|
||
async readFileAsArrayBuffer(_filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null> {
|
||
// In web mode, cannot read arbitrary file paths
|
||
return null;
|
||
}
|
||
|
||
async clearOpenedFiles(): 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();
|