mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +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
+2
-3
@@ -15,6 +15,7 @@ import ErrorNotification from '@app/components/tools/shared/ErrorNotification';
|
||||
import ResultsPreview from '@app/components/tools/shared/ResultsPreview';
|
||||
import BookmarkEditor from '@app/components/tools/editTableOfContents/BookmarkEditor';
|
||||
import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology';
|
||||
import { downloadFromUrl } from '@app/services/downloadService';
|
||||
|
||||
export interface EditTableOfContentsWorkbenchViewData {
|
||||
bookmarks: BookmarkNode[];
|
||||
@@ -177,10 +178,8 @@ const EditTableOfContentsWorkbenchView = ({ data }: EditTableOfContentsWorkbench
|
||||
<Group justify="flex-end" gap="sm">
|
||||
{downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
download={downloadFilename ?? undefined}
|
||||
leftSection={<LocalIcon icon='download-rounded' />}
|
||||
onClick={() => downloadFromUrl(downloadUrl, downloadFilename ?? "download")}
|
||||
>
|
||||
{terminology.download}
|
||||
</Button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react';
|
||||
import { Alert, Button, Group, Loader, Stack, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { GetPdfInfoOperationHook } from '@app/hooks/tools/getPdfInfo/useGetPdfInfoOperation';
|
||||
import { downloadFile } from '@app/services/downloadService';
|
||||
|
||||
interface GetPdfInfoResultsProps {
|
||||
operation: GetPdfInfoOperationHook;
|
||||
@@ -21,14 +22,7 @@ const GetPdfInfoResults = ({ operation, isLoading, errorMessage }: GetPdfInfoRes
|
||||
const selectedDownloadLabel = useMemo(() => t('getPdfInfo.downloadJson', 'Download JSON'), [t]);
|
||||
|
||||
const handleDownload = useCallback((file: File) => {
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
void downloadFile({ data: file, filename: file.name });
|
||||
}, []);
|
||||
|
||||
if (isLoading && operation.results.length === 0) {
|
||||
@@ -76,4 +70,3 @@ const GetPdfInfoResults = ({ operation, isLoading, errorMessage }: GetPdfInfoRes
|
||||
|
||||
export default GetPdfInfoResults;
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@ import { ToolOperationHook } from "@app/hooks/tools/shared/useToolOperation";
|
||||
import { Tooltip } from "@app/components/shared/Tooltip";
|
||||
import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology";
|
||||
import { useFileActionIcons } from "@app/hooks/useFileActionIcons";
|
||||
import { saveOperationResults } from "@app/services/operationResultsSaveService";
|
||||
import { useFileActions, useFileState } from "@app/contexts/FileContext";
|
||||
import { FileId } from "@app/types/fileContext";
|
||||
|
||||
export interface ReviewToolStepProps<TParams = unknown> {
|
||||
isVisible: boolean;
|
||||
@@ -34,6 +37,8 @@ function ReviewStepContent<TParams = unknown>({
|
||||
const icons = useFileActionIcons();
|
||||
const DownloadIcon = icons.download;
|
||||
const stepRef = useRef<HTMLDivElement>(null);
|
||||
const { actions: fileActions } = useFileActions();
|
||||
const { selectors } = useFileState();
|
||||
|
||||
const handleUndo = async () => {
|
||||
try {
|
||||
@@ -50,6 +55,31 @@ function ReviewStepContent<TParams = unknown>({
|
||||
thumbnail: operation.thumbnails[index],
|
||||
})) || [];
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!operation.downloadUrl) return;
|
||||
try {
|
||||
await saveOperationResults({
|
||||
downloadUrl: operation.downloadUrl,
|
||||
downloadFilename: operation.downloadFilename || "download",
|
||||
downloadLocalPath: operation.downloadLocalPath,
|
||||
outputFileIds: operation.outputFileIds,
|
||||
getFile: (fileId) => selectors.getFile(fileId as FileId),
|
||||
getStub: (fileId) => selectors.getStirlingFileStub(fileId as FileId),
|
||||
markSaved: (fileId, savedPath) => {
|
||||
const stub = selectors.getStirlingFileStub(fileId as FileId);
|
||||
fileActions.updateStirlingFileStub(fileId as FileId, {
|
||||
localFilePath: stub?.localFilePath ?? savedPath,
|
||||
isDirty: false
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error("[ReviewToolStep] Failed to download file:", message);
|
||||
alert(`Failed to download file: ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-scroll to bottom when content appears
|
||||
useEffect(() => {
|
||||
if (stepRef.current && (previewFiles.length > 0 || operation.downloadUrl || operation.errorMessage)) {
|
||||
@@ -92,13 +122,11 @@ function ReviewStepContent<TParams = unknown>({
|
||||
)}
|
||||
{operation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={operation.downloadUrl}
|
||||
download={operation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="blue"
|
||||
fullWidth
|
||||
mb="md"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
{terminology.download}
|
||||
</Button>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { downloadFromUrl } from "@app/services/downloadService";
|
||||
|
||||
export type ShowJsTokenType = "kw" | "str" | "num" | "com" | "plain";
|
||||
export type ShowJsToken = { type: ShowJsTokenType; text: string };
|
||||
|
||||
@@ -372,11 +374,6 @@ export async function copyTextToClipboard(text: string, fallbackElement?: HTMLEl
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerDownload(url: string, filename: string): void {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
export async function triggerDownload(url: string, filename: string): Promise<void> {
|
||||
await downloadFromUrl(url, filename);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ValidateSignatureOperationHook } from '@app/hooks/tools/validateSi
|
||||
import '@app/components/tools/validateSignature/reportView/styles.css';
|
||||
import FitText from '@app/components/shared/FitText';
|
||||
import { SuggestedToolsSection } from '@app/components/tools/shared/SuggestedToolsSection';
|
||||
import { downloadFile } from '@app/services/downloadService';
|
||||
|
||||
interface ValidateSignatureResultsProps {
|
||||
operation: ValidateSignatureOperationHook;
|
||||
@@ -80,14 +81,7 @@ const ValidateSignatureResults = ({
|
||||
];
|
||||
|
||||
const handleDownload = useCallback((file: File) => {
|
||||
const blobUrl = URL.createObjectURL(file);
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = file.name;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
void downloadFile({ data: file, filename: file.name });
|
||||
}, []);
|
||||
|
||||
// Show the big loader only while we're still waiting for the first results.
|
||||
|
||||
Reference in New Issue
Block a user