Preserve local paths for desktop saves
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user