open as worspace implementation

This commit is contained in:
2026-05-17 02:31:50 +02:00
parent 07f4361573
commit a5dc70aabf
13 changed files with 445 additions and 162 deletions

View File

@@ -17,6 +17,7 @@ import type {
import {
createInitialPageRefs,
createPageRefId,
createPdfId,
createWorkspaceId,
defaultWorkspaceNameFromPdfName,
normalizeRotation,
@@ -37,6 +38,11 @@ import {
} from "./pdf/pdfService";
import { usePdfThumbnails } from "./pdf/usePdfThumbnails";
import { usePdfGeneratedOutputs } from "./hooks/usePdfGeneratedOutputs";
import {
createSelectionPdfName,
createSelectionWorkspaceName,
getSelectedPagesInVisualOrder,
} from './workspace/workspaceSelection';
function isEditableKeyboardTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
@@ -127,6 +133,21 @@ const App: React.FC = () => {
onError: handleThumbnailError,
});
const closeActionDialog = useCallback(() => {
setActionDialog(null);
}, []);
const openActionDialog = useCallback(
(dialog: {
title: string;
content: React.ReactNode;
actions: ActionDialogAction[];
}) => {
setActionDialog(dialog);
},
[]
);
const refreshWorkspaces = async () => {
try {
const summaries = await listWorkspaces();
@@ -668,7 +689,41 @@ const App: React.FC = () => {
setLastSelectedVisualIndex(null);
};
const handleDeleteSelected = () => {
const performDeleteSelected = useCallback(
(pageIdsToDelete: string[]) => {
if (pageIdsToDelete.length === 0) return;
const before = getCurrentCommandState();
const selectedSet = new Set(pageIdsToDelete);
executeWorkspaceCommand(
createWorkspaceCommand({
type: 'pages.delete',
label:
pageIdsToDelete.length === 1
? 'Deleted selected page'
: `Deleted ${pageIdsToDelete.length} selected pages`,
before,
after: {
pages: pages.filter((page) => !selectedSet.has(page.id)),
selectedPageIds: [],
lastSelectedVisualIndex: null,
},
details: {
count: pageIdsToDelete.length,
},
})
);
},
[
createWorkspaceCommand,
executeWorkspaceCommand,
getCurrentCommandState,
pages,
]
);
const handleDeleteSelected = useCallback(() => {
if (selectedPageIds.length === 0) return;
const idsToDelete = [...selectedPageIds];
@@ -706,33 +761,12 @@ const App: React.FC = () => {
},
],
});
};
const performDeleteSelected = (pageIdsToDelete: string[]) => {
if (pageIdsToDelete.length === 0) return;
const before = getCurrentCommandState();
const selectedSet = new Set(pageIdsToDelete);
executeWorkspaceCommand(
createWorkspaceCommand({
type: "pages.delete",
label:
pageIdsToDelete.length === 1
? "Deleted selected page"
: `Deleted ${pageIdsToDelete.length} selected pages`,
before,
after: {
pages: pages.filter((page) => !selectedSet.has(page.id)),
selectedPageIds: [],
lastSelectedVisualIndex: null,
},
details: {
count: pageIdsToDelete.length,
},
}),
);
};
}, [
closeActionDialog,
openActionDialog,
performDeleteSelected,
selectedPageIds,
]);
const handleCopyPagesToSlot = (pageIds: string[], insertSlot: number) => {
if (!pdf || pageIds.length === 0) return;
@@ -780,18 +814,6 @@ const App: React.FC = () => {
);
};
const closeActionDialog = () => {
setActionDialog(null);
};
const openActionDialog = (dialog: {
title: string;
content: React.ReactNode;
actions: ActionDialogAction[];
}) => {
setActionDialog(dialog);
};
const handleOpenPreview = (pageId: string) => {
setPreviewPageId(pageId);
};
@@ -909,8 +931,13 @@ const App: React.FC = () => {
setIsBusy(true);
try {
const selectedSet = new Set(selectedPageIds);
const selectedPages = pages.filter((page) => selectedSet.has(page.id));
const selectedPages = getSelectedPagesInVisualOrder(
pages,
selectedPageIds
);
if (selectedPages.length === 0) return;
const blob = await exportPages(pdf, selectedPages);
const base = pdf.name.replace(/\.pdf$/i, "");
const filename = `${base}_selected.pdf`;
@@ -923,6 +950,117 @@ const App: React.FC = () => {
}
};
const performOpenSelectionAsWorkspace = async () => {
if (!pdf || selectedPageIds.length === 0) return;
const selectedPages = getSelectedPagesInVisualOrder(pages, selectedPageIds);
if (selectedPages.length === 0) return;
setError(null);
setIsBusy(true);
try {
const selectedPageCount = selectedPages.length;
const blob = await exportPages(pdf, selectedPages);
const arrayBuffer = await blob.arrayBuffer();
const doc = await PDFDocument.load(arrayBuffer);
const pdfName = createSelectionPdfName(pdf.name, selectedPageCount);
const workspaceName = createSelectionWorkspaceName(
pdf.name,
selectedPageCount
);
const extractedPdf: PdfFile = {
id: createPdfId(),
name: pdfName,
doc,
pageCount: doc.getPageCount(),
arrayBuffer,
};
setPdf(extractedPdf);
replaceWorkspaceState({
pages: createInitialPageRefs(extractedPdf.pageCount),
selectedPageIds: [],
lastSelectedVisualIndex: null,
history: [],
redoHistory: [],
dirty: true,
message: `Created a new workspace from ${selectedPageCount} selected ${
selectedPageCount === 1 ? "page" : "pages"
}.`,
});
setActiveWorkspaceId(null);
setWorkspaceName(workspaceName);
setPreviewPageId(null);
clearGeneratedOutputs();
clearThumbnailCache();
} catch (e) {
console.error(e);
setError("Error while opening selection as a new workspace.");
} finally {
setIsBusy(false);
}
};
const handleOpenSelectionAsWorkspace = () => {
if (!pdf || selectedPageIds.length === 0) return;
const selectedPages = getSelectedPagesInVisualOrder(pages, selectedPageIds);
if (selectedPages.length === 0) return;
if (!workspaceDirty) {
void performOpenSelectionAsWorkspace();
return;
}
openActionDialog({
title: "Open selection as new workspace?",
content: (
<>
<p style={{ marginTop: 0 }}>
This will replace the current in-memory workspace with a new
workspace built from {selectedPages.length}{' '}
{selectedPages.length === 1 ? "selected page" : "selected pages"}.
</p>
<p style={{ marginBottom: 0 }}>
The current workspace has unsaved changes. Do you want to save it
before opening the selection as a new workspace?
</p>
</>
),
actions: [
{
label: "Cancel",
variant: "secondary",
onClick: closeActionDialog,
},
{
label: "Open without saving",
variant: "danger",
onClick: () => {
closeActionDialog();
void performOpenSelectionAsWorkspace();
},
},
{
label: "Save and open",
variant: "primary",
autoFocus: true,
onClick: async () => {
closeActionDialog();
const saved = await handleSaveWorkspace();
if (saved) {
await performOpenSelectionAsWorkspace();
}
},
},
],
});
};
const handleExportReordered = async () => {
if (!pdf || pages.length === 0) return;
setError(null);
@@ -1102,6 +1240,7 @@ const App: React.FC = () => {
selectedCount={selectedPageIds.length}
onSplit={handleSplit}
onExtractSelected={handleExtractSelected}
onOpenSelectionAsWorkspace={handleOpenSelectionAsWorkspace}
onExportReordered={handleExportReordered}
splitDownloads={splitDownloads}
subsetDownload={subsetDownload}