mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Preserve local paths for desktop saves (#5543)
# 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]>
This commit is contained in:
co-authored by
James Brunton
James Brunton
parent
946196de43
commit
b8ce4e47c1
@@ -11,6 +11,7 @@ import { FILE_EVENTS } from '@app/services/errorUtils';
|
||||
import { getFilenameWithoutExtension } from '@app/utils/fileUtils';
|
||||
import { ResponseHandler } from '@app/utils/toolResponseProcessor';
|
||||
import { createChildStub, generateProcessedFileMetadata } from '@app/contexts/file/fileActions';
|
||||
import { createNewStirlingFileStub } from '@app/types/fileContext';
|
||||
import { ToolOperation } from '@app/types/file';
|
||||
import { ToolId } from '@app/types/toolId';
|
||||
import { ensureBackendReady } from '@app/services/backendReadinessGuard';
|
||||
@@ -140,6 +141,8 @@ export interface ToolOperationHook<TParams = void> {
|
||||
isGeneratingThumbnails: boolean;
|
||||
downloadUrl: string | null;
|
||||
downloadFilename: string;
|
||||
downloadLocalPath?: string | null;
|
||||
outputFileIds?: string[] | null;
|
||||
isLoading: boolean;
|
||||
status: string;
|
||||
errorMessage: string | null;
|
||||
@@ -375,7 +378,10 @@ export const useToolOperation = <TParams>(
|
||||
actions.setGeneratingThumbnails(false);
|
||||
|
||||
actions.setThumbnails(thumbnails);
|
||||
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
||||
const downloadLocalPath =
|
||||
validFiles.length === 1 && processedFiles.length === 1
|
||||
? selectors.getStirlingFileStub(validFiles[0].fileId)?.localFilePath ?? null
|
||||
: null;
|
||||
|
||||
// Replace input files with processed files (consumeFiles handles pinning)
|
||||
const inputFileIds: FileId[] = [];
|
||||
@@ -390,6 +396,9 @@ export const useToolOperation = <TParams>(
|
||||
inputStirlingFileStubs.push(record);
|
||||
} else {
|
||||
console.warn(`No file stub found for file: ${file.name}`);
|
||||
const fallbackStub = createNewStirlingFileStub(file, fileId);
|
||||
inputFileIds.push(fileId);
|
||||
inputStirlingFileStubs.push(fallbackStub);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,6 +446,18 @@ export const useToolOperation = <TParams>(
|
||||
console.debug('[useToolOperation] Consuming files', { inputCount: inputFileIds.length, toConsume: toConsumeInputIds.length });
|
||||
const outputFileIds = await consumeFiles(toConsumeInputIds, outputStirlingFiles, outputStirlingFileStubs);
|
||||
|
||||
if (toConsumeInputIds.length === 1 && outputFileIds.length === 1) {
|
||||
const inputStub = selectors.getStirlingFileStub(toConsumeInputIds[0]);
|
||||
if (inputStub?.localFilePath) {
|
||||
fileActions.updateStirlingFileStub(outputFileIds[0], {
|
||||
localFilePath: inputStub.localFilePath
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pass output file IDs to download info for marking clean after save
|
||||
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename, downloadLocalPath, outputFileIds);
|
||||
|
||||
// Store operation data for undo (only store what we need to avoid memory bloat)
|
||||
lastOperationRef.current = {
|
||||
inputFiles: extractFiles(validFiles), // Convert to File objects for undo
|
||||
@@ -532,7 +553,6 @@ export const useToolOperation = <TParams>(
|
||||
// Undo the consume operation
|
||||
await undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds);
|
||||
|
||||
|
||||
// Clear results and operation tracking
|
||||
resetResults();
|
||||
lastOperationRef.current = null;
|
||||
@@ -565,6 +585,8 @@ export const useToolOperation = <TParams>(
|
||||
isGeneratingThumbnails: state.isGeneratingThumbnails,
|
||||
downloadUrl: state.downloadUrl,
|
||||
downloadFilename: state.downloadFilename,
|
||||
downloadLocalPath: state.downloadLocalPath,
|
||||
outputFileIds: state.outputFileIds,
|
||||
isLoading: state.isLoading,
|
||||
status: state.status,
|
||||
errorMessage: state.errorMessage,
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface OperationState {
|
||||
isGeneratingThumbnails: boolean;
|
||||
downloadUrl: string | null;
|
||||
downloadFilename: string;
|
||||
downloadLocalPath?: string | null;
|
||||
outputFileIds?: string[] | null;
|
||||
isLoading: boolean;
|
||||
status: string;
|
||||
errorMessage: string | null;
|
||||
@@ -23,7 +25,7 @@ type OperationAction =
|
||||
| { type: 'SET_FILES'; payload: File[] }
|
||||
| { type: 'SET_THUMBNAILS'; payload: string[] }
|
||||
| { type: 'SET_GENERATING_THUMBNAILS'; payload: boolean }
|
||||
| { type: 'SET_DOWNLOAD_INFO'; payload: { url: string | null; filename: string } }
|
||||
| { type: 'SET_DOWNLOAD_INFO'; payload: { url: string | null; filename: string; localPath?: string | null; outputFileIds?: string[] | null } }
|
||||
| { type: 'SET_STATUS'; payload: string }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_PROGRESS'; payload: ProcessingProgress | null }
|
||||
@@ -36,6 +38,8 @@ const initialState: OperationState = {
|
||||
isGeneratingThumbnails: false,
|
||||
downloadUrl: null,
|
||||
downloadFilename: '',
|
||||
downloadLocalPath: null,
|
||||
outputFileIds: null,
|
||||
isLoading: false,
|
||||
status: '',
|
||||
errorMessage: null,
|
||||
@@ -53,10 +57,12 @@ const operationReducer = (state: OperationState, action: OperationAction): Opera
|
||||
case 'SET_GENERATING_THUMBNAILS':
|
||||
return { ...state, isGeneratingThumbnails: action.payload };
|
||||
case 'SET_DOWNLOAD_INFO':
|
||||
return {
|
||||
...state,
|
||||
downloadUrl: action.payload.url,
|
||||
downloadFilename: action.payload.filename
|
||||
return {
|
||||
...state,
|
||||
downloadUrl: action.payload.url,
|
||||
downloadFilename: action.payload.filename,
|
||||
downloadLocalPath: action.payload.localPath ?? null,
|
||||
outputFileIds: action.payload.outputFileIds ?? null
|
||||
};
|
||||
case 'SET_STATUS':
|
||||
return { ...state, status: action.payload };
|
||||
@@ -97,8 +103,8 @@ export const useToolState = () => {
|
||||
dispatch({ type: 'SET_GENERATING_THUMBNAILS', payload: generating });
|
||||
}, []);
|
||||
|
||||
const setDownloadInfo = useCallback((url: string | null, filename: string) => {
|
||||
dispatch({ type: 'SET_DOWNLOAD_INFO', payload: { url, filename } });
|
||||
const setDownloadInfo = useCallback((url: string | null, filename: string, localPath?: string | null, outputFileIds?: string[] | null) => {
|
||||
dispatch({ type: 'SET_DOWNLOAD_INFO', payload: { url, filename, localPath, outputFileIds } });
|
||||
}, []);
|
||||
|
||||
const setStatus = useCallback((status: string) => {
|
||||
@@ -136,4 +142,4 @@ export const useToolState = () => {
|
||||
clearError,
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user