mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Feature/v2/right rail (#4255)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
ConnorYoh
Anthony Stirling
parent
61f5221c58
commit
895bcebc7b
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Paper, Group, TextInput, Button, Text } from '@mantine/core';
|
||||
import { Group, TextInput, Button, Text } from '@mantine/core';
|
||||
|
||||
interface BulkSelectionPanelProps {
|
||||
csvInput: string;
|
||||
@@ -15,7 +15,7 @@ const BulkSelectionPanel = ({
|
||||
onUpdatePagesFromCSV,
|
||||
}: BulkSelectionPanelProps) => {
|
||||
return (
|
||||
<Paper p="md" mb="md" withBorder>
|
||||
<>
|
||||
<Group>
|
||||
<TextInput
|
||||
value={csvInput}
|
||||
@@ -35,7 +35,7 @@ const BulkSelectionPanel = ({
|
||||
Selected: {selectedPages.length} pages
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -141,7 +141,6 @@ const FileThumbnail = ({
|
||||
filter: isSupported ? 'none' : 'grayscale(50%)'
|
||||
}}
|
||||
>
|
||||
{selectionMode && (
|
||||
<div
|
||||
className={styles.checkboxContainer}
|
||||
data-testid="file-thumbnail-checkbox"
|
||||
@@ -175,7 +174,6 @@ const FileThumbnail = ({
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File content area */}
|
||||
<div className="file-container w-[90%] h-[80%] relative">
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useState, useCallback, useRef, useEffect, useMemo } from "react";
|
||||
import {
|
||||
Button, Text, Center, Checkbox, Box, Tooltip, ActionIcon,
|
||||
Button, Text, Center, Box,
|
||||
Notification, TextInput, LoadingOverlay, Modal, Alert,
|
||||
Stack, Group
|
||||
Stack, Group, Portal
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext";
|
||||
import { ModeType } from "../../contexts/NavigationContext";
|
||||
import { PDFDocument, PDFPage } from "../../types/pageEditor";
|
||||
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
||||
import { useUndoRedo } from "../../hooks/useUndoRedo";
|
||||
import {
|
||||
RotatePagesCommand,
|
||||
@@ -56,7 +55,6 @@ export interface PageEditorProps {
|
||||
const PageEditor = ({
|
||||
onFunctionsReady,
|
||||
}: PageEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Use split contexts to prevent re-renders
|
||||
const { state, selectors } = useFileState();
|
||||
@@ -241,19 +239,26 @@ const PageEditor = ({
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null);
|
||||
const [exportSelectedOnly, setExportSelectedOnly] = useState<boolean>(false);
|
||||
|
||||
// Animation state
|
||||
const [movingPage, setMovingPage] = useState<number | null>(null);
|
||||
const [pagePositions, setPagePositions] = useState<Map<string, { x: number; y: number }>>(new Map());
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const pageRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const fileInputRef = useRef<() => void>(null);
|
||||
|
||||
// Undo/Redo system
|
||||
const { executeCommand, undo, redo, canUndo, canRedo } = useUndoRedo();
|
||||
|
||||
// Track whether the user has manually edited the filename to avoid auto-overwrites
|
||||
const userEditedFilename = useRef(false);
|
||||
|
||||
// Reset user edit flag when the active files change, so defaults can be applied for new docs
|
||||
useEffect(() => {
|
||||
userEditedFilename.current = false;
|
||||
}, [filesSignature]);
|
||||
|
||||
// Set initial filename when document changes - use stable signature
|
||||
useEffect(() => {
|
||||
if (userEditedFilename.current) return; // Do not overwrite user-typed filename
|
||||
if (mergedPdfDocument) {
|
||||
if (activeFileIds.length === 1 && primaryFileId) {
|
||||
const record = selectors.getFileRecord(primaryFileId);
|
||||
@@ -838,14 +843,18 @@ const PageEditor = ({
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!displayDocument) return;
|
||||
|
||||
const pagesToDelete = selectionMode
|
||||
? selectedPageNumbers.map(pageNum => {
|
||||
const page = displayDocument.pages.find(p => p.pageNumber === pageNum);
|
||||
return page?.id || '';
|
||||
}).filter(id => id)
|
||||
const hasSelectedPages = selectedPageNumbers.length > 0;
|
||||
|
||||
const pagesToDelete = (selectionMode || hasSelectedPages)
|
||||
? selectedPageNumbers
|
||||
.map(pageNum => {
|
||||
const page = displayDocument.pages.find(p => p.pageNumber === pageNum);
|
||||
return page?.id || '';
|
||||
})
|
||||
.filter(id => id)
|
||||
: displayDocument.pages.map(p => p.id);
|
||||
|
||||
if (selectionMode && selectedPageNumbers.length === 0) return;
|
||||
if ((selectionMode || hasSelectedPages) && selectedPageNumbers.length === 0) return;
|
||||
|
||||
const command = new DeletePagesCommand(
|
||||
displayDocument,
|
||||
@@ -857,7 +866,7 @@ const PageEditor = ({
|
||||
if (selectionMode) {
|
||||
actions.setSelectedPages([]);
|
||||
}
|
||||
const pageCount = selectionMode ? selectedPageNumbers.length : displayDocument.pages.length;
|
||||
const pageCount = (selectionMode || hasSelectedPages) ? selectedPageNumbers.length : displayDocument.pages.length;
|
||||
setStatus(`Deleted ${pageCount} pages`);
|
||||
}, [displayDocument, selectedPageNumbers, selectionMode, executeCommand, setPdfDocument, actions]);
|
||||
|
||||
@@ -885,49 +894,52 @@ const PageEditor = ({
|
||||
}, [displayDocument, selectedPageNumbers, selectionMode, executeCommand, setPdfDocument]);
|
||||
|
||||
const showExportPreview = useCallback((selectedOnly: boolean = false) => {
|
||||
if (!mergedPdfDocument) return;
|
||||
const doc = editedDocument || mergedPdfDocument;
|
||||
if (!doc) return;
|
||||
|
||||
// Convert page numbers to page IDs for export service
|
||||
const exportPageIds = selectedOnly
|
||||
? selectedPageNumbers.map(pageNum => {
|
||||
const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum);
|
||||
const page = doc.pages.find(p => p.pageNumber === pageNum);
|
||||
return page?.id || '';
|
||||
}).filter(id => id)
|
||||
: [];
|
||||
|
||||
|
||||
const preview = pdfExportService.getExportInfo(mergedPdfDocument, exportPageIds, selectedOnly);
|
||||
const preview = pdfExportService.getExportInfo(doc, exportPageIds, selectedOnly);
|
||||
setExportPreview(preview);
|
||||
setExportSelectedOnly(selectedOnly);
|
||||
setShowExportModal(true);
|
||||
}, [mergedPdfDocument, selectedPageNumbers]);
|
||||
}, [editedDocument, mergedPdfDocument, selectedPageNumbers]);
|
||||
|
||||
const handleExport = useCallback(async (selectedOnly: boolean = false) => {
|
||||
if (!mergedPdfDocument) return;
|
||||
const doc = editedDocument || mergedPdfDocument;
|
||||
if (!doc) return;
|
||||
|
||||
setExportLoading(true);
|
||||
try {
|
||||
// Convert page numbers to page IDs for export service
|
||||
const exportPageIds = selectedOnly
|
||||
? selectedPageNumbers.map(pageNum => {
|
||||
const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum);
|
||||
const page = doc.pages.find(p => p.pageNumber === pageNum);
|
||||
return page?.id || '';
|
||||
}).filter(id => id)
|
||||
: [];
|
||||
|
||||
|
||||
const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly);
|
||||
const errors = pdfExportService.validateExport(doc, exportPageIds, selectedOnly);
|
||||
if (errors.length > 0) {
|
||||
setStatus(errors.join(', '));
|
||||
return;
|
||||
}
|
||||
|
||||
const hasSplitMarkers = mergedPdfDocument.pages.some(page => page.splitBefore);
|
||||
const hasSplitMarkers = doc.pages.some(page => page.splitBefore);
|
||||
|
||||
if (hasSplitMarkers) {
|
||||
const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, {
|
||||
const result = await pdfExportService.exportPDF(doc, exportPageIds, {
|
||||
selectedOnly,
|
||||
filename,
|
||||
splitDocuments: true
|
||||
splitDocuments: true,
|
||||
appendSuffix: false
|
||||
}) as { blobs: Blob[]; filenames: string[] };
|
||||
|
||||
result.blobs.forEach((blob, index) => {
|
||||
@@ -938,9 +950,10 @@ const PageEditor = ({
|
||||
|
||||
setStatus(`Exported ${result.blobs.length} split documents`);
|
||||
} else {
|
||||
const result = await pdfExportService.exportPDF(mergedPdfDocument, exportPageIds, {
|
||||
const result = await pdfExportService.exportPDF(doc, exportPageIds, {
|
||||
selectedOnly,
|
||||
filename
|
||||
filename,
|
||||
appendSuffix: false
|
||||
}) as { blob: Blob; filename: string };
|
||||
|
||||
pdfExportService.downloadFile(result.blob, result.filename);
|
||||
@@ -953,7 +966,7 @@ const PageEditor = ({
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [mergedPdfDocument, selectedPageNumbers, filename]);
|
||||
}, [editedDocument, mergedPdfDocument, selectedPageNumbers, filename]);
|
||||
|
||||
const handleUndo = useCallback(() => {
|
||||
if (undo()) {
|
||||
@@ -1240,59 +1253,13 @@ const PageEditor = ({
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Group mb="md">
|
||||
<TextInput
|
||||
label="Filename"
|
||||
value={filename}
|
||||
onChange={(e) => setFilename(e.target.value)}
|
||||
placeholder="Enter filename"
|
||||
style={{ minWidth: 200 }}
|
||||
style={{ minWidth: 200, maxWidth: 200, marginLeft: "1rem"}}
|
||||
/>
|
||||
<Button
|
||||
onClick={toggleSelectionMode}
|
||||
variant={selectionMode ? "filled" : "outline"}
|
||||
color={selectionMode ? "blue" : "gray"}
|
||||
styles={{
|
||||
root: {
|
||||
transition: 'all 0.2s ease',
|
||||
...(selectionMode && {
|
||||
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectionMode ? "Exit Selection" : "Select Pages"}
|
||||
</Button>
|
||||
{selectionMode && (
|
||||
<>
|
||||
<Button onClick={selectAll} variant="light">Select All</Button>
|
||||
<Button onClick={deselectAll} variant="light">Deselect All</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* Apply Changes Button */}
|
||||
{hasUnsavedChanges && (
|
||||
<Button
|
||||
onClick={applyChanges}
|
||||
color="green"
|
||||
variant="filled"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
Apply Changes
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{selectionMode && (
|
||||
<BulkSelectionPanel
|
||||
csvInput={csvInput}
|
||||
setCsvInput={setCsvInput}
|
||||
selectedPages={selectedPageNumbers}
|
||||
onUpdatePagesFromCSV={updatePagesFromCSV}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<DragDropGrid
|
||||
@@ -1386,8 +1353,7 @@ const PageEditor = ({
|
||||
loading={exportLoading}
|
||||
onClick={() => {
|
||||
setShowExportModal(false);
|
||||
const selectedOnly = exportPreview.pageCount < (mergedPdfDocument?.pages.length || 0);
|
||||
handleExport(selectedOnly);
|
||||
handleExport(exportSelectedOnly);
|
||||
}}
|
||||
>
|
||||
Export PDF
|
||||
@@ -1446,14 +1412,16 @@ const PageEditor = ({
|
||||
</Modal>
|
||||
|
||||
{status && (
|
||||
<Portal>
|
||||
<Notification
|
||||
color="blue"
|
||||
mt="md"
|
||||
onClose={() => setStatus(null)}
|
||||
style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 1000 }}
|
||||
style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 10000 }}
|
||||
>
|
||||
{status}
|
||||
</Notification>
|
||||
</Portal>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
|
||||
@@ -2,16 +2,12 @@ import React from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
ActionIcon,
|
||||
Paper
|
||||
} from "@mantine/core";
|
||||
import UndoIcon from "@mui/icons-material/Undo";
|
||||
import RedoIcon from "@mui/icons-material/Redo";
|
||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
interface PageEditorControlsProps {
|
||||
// Close/Reset functions
|
||||
@@ -39,17 +35,12 @@ interface PageEditorControlsProps {
|
||||
}
|
||||
|
||||
const PageEditorControls = ({
|
||||
onClosePdf,
|
||||
onUndo,
|
||||
onRedo,
|
||||
canUndo,
|
||||
canRedo,
|
||||
onRotate,
|
||||
onDelete,
|
||||
onSplit,
|
||||
onExportSelected,
|
||||
onExportAll,
|
||||
exportLoading,
|
||||
selectionMode,
|
||||
selectedPages
|
||||
}: PageEditorControlsProps) => {
|
||||
@@ -57,9 +48,9 @@ const PageEditorControls = ({
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
bottom: '20px',
|
||||
transform: 'translateX(-50%)',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 50,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -67,34 +58,28 @@ const PageEditorControls = ({
|
||||
background: 'transparent',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
radius="xl"
|
||||
shadow="lg"
|
||||
p={16}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
borderRadius: 32,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
boxShadow: '0 -2px 8px rgba(0,0,0,0.04)',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
border: '1px solid var(--border-default)',
|
||||
borderRadius: '16px 16px 0 0',
|
||||
pointerEvents: 'auto',
|
||||
minWidth: 400,
|
||||
justifyContent: 'center'
|
||||
minWidth: 420,
|
||||
maxWidth: 700,
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
padding: "1rem",
|
||||
paddingBottom: "2rem"
|
||||
}}
|
||||
>
|
||||
{/* Close PDF */}
|
||||
<Tooltip label="Close PDF">
|
||||
<ActionIcon
|
||||
onClick={onClosePdf}
|
||||
color="red"
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
<CloseIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
|
||||
|
||||
{/* Undo/Redo */}
|
||||
<Tooltip label="Undo">
|
||||
@@ -133,17 +118,6 @@ const PageEditorControls = ({
|
||||
<RotateRightIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={selectionMode ? "Delete Selected" : "Delete All"}>
|
||||
<ActionIcon
|
||||
onClick={onDelete}
|
||||
disabled={selectionMode && selectedPages.length === 0}
|
||||
color="red"
|
||||
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
|
||||
size="lg"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={selectionMode ? "Split Selected" : "Split All"}>
|
||||
<ActionIcon
|
||||
onClick={onSplit}
|
||||
@@ -156,34 +130,7 @@ const PageEditorControls = ({
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
|
||||
|
||||
{/* Export Controls */}
|
||||
{selectionMode && selectedPages.length > 0 && (
|
||||
<Tooltip label="Export Selected">
|
||||
<ActionIcon
|
||||
onClick={onExportSelected}
|
||||
disabled={exportLoading}
|
||||
color="blue"
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="Export All">
|
||||
<ActionIcon
|
||||
onClick={onExportAll}
|
||||
disabled={exportLoading}
|
||||
color="green"
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Paper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -205,7 +205,7 @@ const PageThumbnail = React.memo(({
|
||||
}}
|
||||
draggable={false}
|
||||
>
|
||||
{selectionMode && (
|
||||
{
|
||||
<div
|
||||
className={styles.checkboxContainer}
|
||||
style={{
|
||||
@@ -213,10 +213,9 @@ const PageThumbnail = React.memo(({
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 10,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
border: '1px solid #ccc',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '4px',
|
||||
padding: '4px',
|
||||
padding: '2px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
pointerEvents: 'auto',
|
||||
cursor: 'pointer'
|
||||
@@ -239,7 +238,7 @@ const PageThumbnail = React.memo(({
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
|
||||
<div className="page-container w-[90%] h-[90%]" draggable={false}>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user