mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
## Description Adds an explicit **“Save As”** button to the desktop viewer so users can always save a copy of the current PDF to a different location, even if the original file already has a local path. This complements the existing smart **Save/Download** behavior: - The existing download button continues to either save back to the original path (when available) or prompt for a path when needed. - The new **Save As** button always opens a save dialog to choose a location/name for a new copy. ## Changes - **RightRail (viewer controls)** - Added a new **Save As** action icon in the right rail settings section. - The button: - Uses `viewerContext.exportActions.saveAsCopy()` to get the current viewer state as a PDF. - Calls `downloadFile` without a `localPath`, ensuring the desktop app shows a **Save As** dialog. - Picks the first selected file (if any) or the first active file as the source for the filename. - **Desktop / Web behavior** - In the desktop app (Tauri), clicking **Save As**: - Opens a native save dialog so the user can choose a different folder and filename. - Writes a new copy without changing the existing file’s `localFilePath` or dirty state. - In the web app, the button behaves like a standard download of a copy (browser-controlled save dialog / download). ## Motivation - Users often want to apply operations on a PDF while **keeping the original unmodified**. - The existing smart Save behavior chooses between Save and Save As automatically, but there was no way to explicitly request **Save As**. - This change gives desktop users a clear, dedicated **“Save As”** control while preserving the current Save/Download behavior. ## Notes - No backend changes. - No changes to the existing Save / Download button behavior. - The new button uses existing viewer export and download utilities, minimizing new logic. --------- Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
co-authored by
James Brunton
parent
a7f2abcb22
commit
55bcb92810
@@ -134,68 +134,71 @@ export default function RightRail() {
|
||||
[actions, allButtonsDisabled, disableForFullscreen, tooltipPosition, tooltipOffset]
|
||||
);
|
||||
|
||||
const handleExportAll = useCallback(async () => {
|
||||
if (currentView === 'viewer') {
|
||||
const buffer = await viewerContext?.exportActions?.saveAsCopy?.();
|
||||
if (!buffer) return;
|
||||
const fileToExport = selectedFiles.length > 0 ? selectedFiles[0] : activeFiles[0];
|
||||
if (!fileToExport) return;
|
||||
const stub = isStirlingFile(fileToExport) ? selectors.getStirlingFileStub(fileToExport.fileId) : undefined;
|
||||
try {
|
||||
const result = await downloadFile({
|
||||
data: new Blob([buffer], { type: 'application/pdf' }),
|
||||
filename: fileToExport.name,
|
||||
localPath: stub?.localFilePath,
|
||||
});
|
||||
if (!result.cancelled && stub && result.savedPath) {
|
||||
fileActions.updateStirlingFileStub(stub.id, {
|
||||
localFilePath: stub.localFilePath ?? result.savedPath,
|
||||
isDirty: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RightRail] Failed to export viewer file:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentView === 'pageEditor') {
|
||||
pageEditorFunctions?.onExportAll?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles;
|
||||
|
||||
if (filesToExport.length > 0) {
|
||||
for (const file of filesToExport) {
|
||||
const stub = isStirlingFile(file) ? selectors.getStirlingFileStub(file.fileId) : undefined;
|
||||
const handleExportAll = useCallback(
|
||||
async (forceNewFile = false) => {
|
||||
if (currentView === 'viewer') {
|
||||
const buffer = await viewerContext?.exportActions?.saveAsCopy?.();
|
||||
if (!buffer) return;
|
||||
const fileToExport = selectedFiles.length > 0 ? selectedFiles[0] : activeFiles[0];
|
||||
if (!fileToExport) return;
|
||||
const stub = isStirlingFile(fileToExport) ? selectors.getStirlingFileStub(fileToExport.fileId) : undefined;
|
||||
try {
|
||||
const result = await downloadFile({
|
||||
data: file,
|
||||
filename: file.name,
|
||||
localPath: stub?.localFilePath
|
||||
data: new Blob([buffer], { type: 'application/pdf' }),
|
||||
filename: fileToExport.name,
|
||||
localPath: forceNewFile ? undefined : stub?.localFilePath,
|
||||
});
|
||||
if (result.cancelled) continue;
|
||||
if (stub && result.savedPath) {
|
||||
if (!forceNewFile && !result.cancelled && stub && result.savedPath) {
|
||||
fileActions.updateStirlingFileStub(stub.id, {
|
||||
localFilePath: stub.localFilePath ?? result.savedPath,
|
||||
isDirty: false
|
||||
isDirty: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RightRail] Failed to export file:', file.name, error);
|
||||
console.error('[RightRail] Failed to export viewer file:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentView === 'pageEditor') {
|
||||
pageEditorFunctions?.onExportAll?.();
|
||||
return;
|
||||
}
|
||||
|
||||
const filesToExport = selectedFiles.length > 0 ? selectedFiles : activeFiles;
|
||||
|
||||
if (filesToExport.length > 0) {
|
||||
for (const file of filesToExport) {
|
||||
const stub = isStirlingFile(file) ? selectors.getStirlingFileStub(file.fileId) : undefined;
|
||||
try {
|
||||
const result = await downloadFile({
|
||||
data: file,
|
||||
filename: file.name,
|
||||
localPath: forceNewFile ? undefined : stub?.localFilePath,
|
||||
});
|
||||
if (result.cancelled) continue;
|
||||
if (!forceNewFile && stub && result.savedPath) {
|
||||
fileActions.updateStirlingFileStub(stub.id, {
|
||||
localFilePath: stub.localFilePath ?? result.savedPath,
|
||||
isDirty: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RightRail] Failed to export file:', file.name, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentView,
|
||||
selectedFiles,
|
||||
activeFiles,
|
||||
pageEditorFunctions,
|
||||
viewerContext,
|
||||
selectors,
|
||||
fileActions,
|
||||
]);
|
||||
},
|
||||
[
|
||||
currentView,
|
||||
selectedFiles,
|
||||
activeFiles,
|
||||
pageEditorFunctions,
|
||||
viewerContext,
|
||||
selectors,
|
||||
fileActions,
|
||||
]
|
||||
);
|
||||
|
||||
const downloadTooltip = useMemo(() => {
|
||||
if (currentView === 'pageEditor') {
|
||||
@@ -264,7 +267,7 @@ export default function RightRail() {
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={handleExportAll}
|
||||
onClick={() => handleExportAll()}
|
||||
disabled={
|
||||
disableForFullscreen ||
|
||||
(currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled))
|
||||
@@ -276,6 +279,24 @@ export default function RightRail() {
|
||||
tooltipPosition,
|
||||
tooltipOffset
|
||||
)}
|
||||
{icons.saveAsIconName &&
|
||||
renderWithTooltip(
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => handleExportAll(true)}
|
||||
disabled={
|
||||
disableForFullscreen ||
|
||||
(currentView !== 'viewer' && (totalItems === 0 || allButtonsDisabled))
|
||||
}
|
||||
>
|
||||
<LocalIcon icon={icons.saveAsIconName} width="1.5rem" height="1.5rem" />
|
||||
</ActionIcon>,
|
||||
t('rightRail.saveAs', 'Save As'),
|
||||
tooltipPosition,
|
||||
tooltipOffset
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="right-rail-spacer" />
|
||||
|
||||
@@ -2,8 +2,9 @@ import UploadIcon from '@mui/icons-material/Upload';
|
||||
import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined';
|
||||
|
||||
/**
|
||||
* File action icons for web builds
|
||||
* Desktop builds override this with different icons
|
||||
* File action icons for web builds.
|
||||
* Desktop builds override this file via TypeScript path aliases to provide
|
||||
* different icons (e.g. Save icon instead of Download, and a Save As icon).
|
||||
*/
|
||||
export function useFileActionIcons() {
|
||||
return {
|
||||
@@ -11,5 +12,8 @@ export function useFileActionIcons() {
|
||||
download: DownloadOutlinedIcon,
|
||||
uploadIconName: 'upload' as const,
|
||||
downloadIconName: 'download' as const,
|
||||
// Web builds do not expose a Save As icon — the button is hidden when this is undefined.
|
||||
// Desktop builds override this file and return a real icon name.
|
||||
saveAsIconName: undefined as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import FolderOpenOutlinedIcon from '@mui/icons-material/FolderOpenOutlined';
|
||||
import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined';
|
||||
|
||||
/**
|
||||
* File action icons for desktop builds
|
||||
* Overrides core implementation with desktop-appropriate icons
|
||||
* File action icons for desktop builds.
|
||||
* Overrides the core implementation with desktop-appropriate icons.
|
||||
* The presence of `saveAsIconName` signals RightRail to show the Save As button.
|
||||
*/
|
||||
export function useFileActionIcons() {
|
||||
return {
|
||||
@@ -11,5 +12,8 @@ export function useFileActionIcons() {
|
||||
download: SaveOutlinedIcon,
|
||||
uploadIconName: 'folder-rounded' as const,
|
||||
downloadIconName: 'save-rounded' as const,
|
||||
// Returning this icon name causes RightRail to render the Save As button.
|
||||
// On desktop, downloadFile() without a localPath shows a native save dialog.
|
||||
saveAsIconName: 'save-as-rounded' as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user