Preserve local paths for desktop saves

This commit is contained in:
Anthony Stirling
2026-01-23 17:48:51 +00:00
parent 0436460c03
commit c987deb31b
4 changed files with 130 additions and 25 deletions
@@ -9,6 +9,7 @@ import { pdfExportService } from "@app/services/pdfExportService";
import { exportProcessedDocumentsToFiles } from "@app/services/pdfExportHelpers";
import { FileId } from "@app/types/file";
import { PDFDocument } from "@app/types/pageEditor";
import { isTauri } from "@tauri-apps/api/core";
type FileActions = ReturnType<typeof useFileActions>["actions"];
type FileSelectors = ReturnType<typeof useFileState>["selectors"];
@@ -68,6 +69,23 @@ export const usePageEditorExport = ({
setExportLoading,
setSplitPositions,
}: UsePageEditorExportParams) => {
const saveBlobToLocalPath = useCallback(async (blob: Blob, filePath?: string): Promise<boolean> => {
if (!filePath || !isTauri()) {
return false;
}
const { writeFile } = await import("@tauri-apps/plugin-fs");
try {
const arrayBuffer = await blob.arrayBuffer();
await writeFile(filePath, new Uint8Array(arrayBuffer));
return true;
} catch (error) {
console.error("[PageEditor] Failed to save file to local path:", error);
return false;
}
}, []);
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
const sourceFiles = new Map<FileId, File>();
@@ -144,7 +162,13 @@ export const usePageEditorExport = ({
{ selectedOnly: true, filename: exportFilename }
);
pdfExportService.downloadFile(result.blob, result.filename);
const selectedFileId = selectedFileIds.length === 1 ? selectedFileIds[0] : null;
const selectedStub = selectedFileId ? selectors.getStirlingFileStub(selectedFileId) : null;
const savedToLocalPath = await saveBlobToLocalPath(result.blob, selectedStub?.localFilePath);
if (!savedToLocalPath) {
pdfExportService.downloadFile(result.blob, result.filename);
}
setHasUnsavedChanges(false);
setSplitPositions(new Set());
setExportLoading(false);
@@ -158,6 +182,9 @@ export const usePageEditorExport = ({
splitPositions,
getSourceFiles,
getExportFilename,
selectedFileIds,
selectors,
saveBlobToLocalPath,
setHasUnsavedChanges,
setExportLoading,
]);
@@ -207,7 +234,13 @@ export const usePageEditorExport = ({
pdfExportService.downloadFile(zipBlob, zipFilename);
} else {
const file = files[0];
pdfExportService.downloadFile(file, file.name);
const selectedFileId = selectedFileIds.length === 1 ? selectedFileIds[0] : null;
const selectedStub = selectedFileId ? selectors.getStirlingFileStub(selectedFileId) : null;
const savedToLocalPath = await saveBlobToLocalPath(file, selectedStub?.localFilePath);
if (!savedToLocalPath) {
pdfExportService.downloadFile(file, file.name);
}
}
setHasUnsavedChanges(false);
@@ -222,6 +255,9 @@ export const usePageEditorExport = ({
splitPositions,
getSourceFiles,
getExportFilename,
selectedFileIds,
selectors,
saveBlobToLocalPath,
setHasUnsavedChanges,
setExportLoading,
]);
@@ -22,6 +22,8 @@ import LightModeIcon from '@mui/icons-material/LightMode';
import { useSidebarContext } from '@app/contexts/SidebarContext';
import { RightRailButtonConfig, RightRailRenderContext, RightRailSection } from '@app/types/rightRail';
import { useRightRailTooltipSide } from '@app/hooks/useRightRailTooltipSide';
import { isTauri } from '@tauri-apps/api/core';
import { isStirlingFile } from '@app/types/fileContext';
const SECTION_ORDER: RightRailSection[] = ['top', 'middle', 'bottom'];
@@ -62,6 +64,7 @@ export default function RightRail() {
const { signaturesApplied } = useSignature();
const activeFiles = selectors.getFiles();
const activeFileIndex = viewerContext?.activeFileIndex ?? 0;
const pageEditorTotalPages = pageEditorFunctions?.totalPages ?? 0;
const pageEditorSelectedCount = pageEditorFunctions?.selectedPageIds?.length ?? 0;
const exportState = viewerContext?.getExportState?.();
@@ -135,13 +138,48 @@ export default function RightRail() {
[actions, allButtonsDisabled, disableForFullscreen, tooltipPosition, tooltipOffset]
);
const saveToLocalPathIfPossible = useCallback(async (file: File, filePath?: string | null) => {
if (!filePath || !isTauri()) {
return false;
}
const { writeFile } = await import('@tauri-apps/plugin-fs');
try {
const arrayBuffer = await file.arrayBuffer();
await writeFile(filePath, new Uint8Array(arrayBuffer));
return true;
} catch (error) {
console.error('[RightRail] Failed to save file to local path:', error);
return false;
}
}, []);
const handleExportAll = useCallback(async () => {
if (currentView === 'viewer') {
if (!signaturesApplied) {
alert('You have unapplied signatures. Please use "Apply Signatures" first before exporting.');
return;
}
viewerContext?.exportActions?.download();
const activeFile = activeFiles[activeFileIndex] ?? activeFiles[0];
const activeFileId = isStirlingFile(activeFile) ? activeFile.fileId : undefined;
const activeStub = activeFileId ? selectors.getStirlingFileStub(activeFileId) : undefined;
const localFilePath = activeStub?.localFilePath;
if (localFilePath && viewerContext?.exportActions?.saveAsCopy && isTauri()) {
const { writeFile } = await import('@tauri-apps/plugin-fs');
try {
const arrayBuffer = await viewerContext.exportActions.saveAsCopy();
if (arrayBuffer) {
await writeFile(localFilePath, new Uint8Array(arrayBuffer));
return;
}
} catch (error) {
console.error('[RightRail] Failed to overwrite local PDF:', error);
}
}
viewerContext?.exportActions?.download?.();
return;
}
@@ -151,7 +189,16 @@ export default function RightRail() {
}
const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
filesToDownload.forEach(file => {
for (const file of filesToDownload) {
const fileId = isStirlingFile(file) ? file.fileId : undefined;
const stub = fileId ? selectors.getStirlingFileStub(fileId) : undefined;
const localFilePath = stub?.localFilePath;
const savedToLocalPath = await saveToLocalPathIfPossible(file, localFilePath);
if (savedToLocalPath) {
continue;
}
const link = document.createElement('a');
link.href = URL.createObjectURL(file);
link.download = file.name;
@@ -159,14 +206,17 @@ export default function RightRail() {
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
});
}
}, [
currentView,
selectedFiles,
activeFiles,
pageEditorFunctions,
viewerContext,
signaturesApplied
signaturesApplied,
activeFileIndex,
selectors,
saveToLocalPathIfPossible,
]);
const downloadTooltip = useMemo(() => {
+1
View File
@@ -45,6 +45,7 @@ export interface StirlingFileStub extends BaseFileMetadata {
quickKey?: string; // Fast deduplication key: name|size|lastModified
thumbnailUrl?: string; // Generated thumbnail blob URL for visual display
blobUrl?: string; // File access blob URL for downloads/processing
localFilePath?: string; // Original local filesystem path (desktop app only)
processedFile?: ProcessedFileMetadata; // PDF page data and processing results
insertAfterPageId?: string; // Page ID after which this file should be inserted
isPinned?: boolean; // Protected from tool consumption (replace/remove)
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useOpenedFile } from '@app/hooks/useOpenedFile';
import { fileOpenService } from '@app/services/fileOpenService';
import { useFileManagement } from '@app/contexts/file/fileHooks';
import { createQuickKey } from '@app/types/fileContext';
/**
* App initialization hook
@@ -11,7 +12,7 @@ import { useFileManagement } from '@app/contexts/file/fileHooks';
*/
export function useAppInitialization(): void {
// Get file management actions
const { addFiles } = useFileManagement();
const { addFiles, updateStirlingFileStub } = useFileManagement();
// Handle files opened with app (Tauri mode)
const { openedFilePaths, loading: openedFileLoading } = useOpenedFile();
@@ -24,28 +25,45 @@ export function useAppInitialization(): void {
const loadOpenedFiles = async () => {
try {
const filesArray: File[] = [];
const loadedFiles = (
await Promise.all(
openedFilePaths.map(async (filePath) => {
try {
const fileData = await fileOpenService.readFileAsArrayBuffer(filePath);
if (!fileData) return null;
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) {
await addFiles(filesArray);
console.log(`[Desktop] ${filesArray.length} opened file(s) added to FileContext`);
console.log('[Desktop] Loaded file:', fileData.fileName);
return {
file,
filePath,
quickKey: createQuickKey(file),
};
} catch (error) {
console.error('[Desktop] Failed to load file:', filePath, error);
return null;
}
})
)
).filter((entry): entry is { file: File; filePath: string; quickKey: string } => Boolean(entry));
if (loadedFiles.length > 0) {
const filesArray = loadedFiles.map(entry => entry.file);
const quickKeyToPath = new Map(loadedFiles.map(entry => [entry.quickKey, entry.filePath]));
const addedFiles = await addFiles(filesArray);
addedFiles.forEach(file => {
const localFilePath = quickKeyToPath.get(file.quickKey);
if (localFilePath) {
updateStirlingFileStub(file.fileId, { localFilePath });
}
});
console.log(`[Desktop] ${loadedFiles.length} opened file(s) added to FileContext`);
}
} catch (error) {
console.error('[Desktop] Failed to load opened files:', error);
@@ -53,7 +71,7 @@ export function useAppInitialization(): void {
};
loadOpenedFiles();
}, [openedFilePaths, openedFileLoading, addFiles]);
}, [openedFilePaths, openedFileLoading, addFiles, updateStirlingFileStub]);
}
export function useSetupCompletion(): (completed: boolean) => void {