mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-27 22:59:22 +00:00
Feature/v2/page editor selection persistance (#4306)
Fixed page editor selection persistance Fixed delete sleected Fixed display issue on some tools on reset settings call --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This commit is contained in:
parent
47ccb6a6ed
commit
68d59fd377
@ -117,7 +117,8 @@ export default function Workbench() {
|
||||
onExportAll={pageEditorFunctions.onExportAll}
|
||||
exportLoading={pageEditorFunctions.exportLoading}
|
||||
selectionMode={pageEditorFunctions.selectionMode}
|
||||
selectedPages={pageEditorFunctions.selectedPages}
|
||||
selectedPageIds={pageEditorFunctions.selectedPageIds}
|
||||
displayDocument={pageEditorFunctions.displayDocument}
|
||||
splitPositions={pageEditorFunctions.splitPositions}
|
||||
totalPages={pageEditorFunctions.totalPages}
|
||||
/>
|
||||
|
@ -4,14 +4,16 @@ import { Group, TextInput, Button, Text } from '@mantine/core';
|
||||
interface BulkSelectionPanelProps {
|
||||
csvInput: string;
|
||||
setCsvInput: (value: string) => void;
|
||||
selectedPages: number[];
|
||||
selectedPageIds: string[];
|
||||
displayDocument?: { pages: { id: string; pageNumber: number }[] };
|
||||
onUpdatePagesFromCSV: () => void;
|
||||
}
|
||||
|
||||
const BulkSelectionPanel = ({
|
||||
csvInput,
|
||||
setCsvInput,
|
||||
selectedPages,
|
||||
selectedPageIds,
|
||||
displayDocument,
|
||||
onUpdatePagesFromCSV,
|
||||
}: BulkSelectionPanelProps) => {
|
||||
return (
|
||||
@ -30,9 +32,12 @@ const BulkSelectionPanel = ({
|
||||
Apply
|
||||
</Button>
|
||||
</Group>
|
||||
{selectedPages.length > 0 && (
|
||||
{selectedPageIds.length > 0 && (
|
||||
<Text size="sm" c="dimmed" mt="sm">
|
||||
Selected: {selectedPages.length} pages
|
||||
Selected: {selectedPageIds.length} pages ({displayDocument ? selectedPageIds.map(id => {
|
||||
const page = displayDocument.pages.find(p => p.id === id);
|
||||
return page?.pageNumber || 0;
|
||||
}).filter(n => n > 0).join(', ') : ''})
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
|
@ -12,10 +12,10 @@ interface DragDropItem {
|
||||
|
||||
interface DragDropGridProps<T extends DragDropItem> {
|
||||
items: T[];
|
||||
selectedItems: number[];
|
||||
selectedItems: string[];
|
||||
selectionMode: boolean;
|
||||
isAnimating: boolean;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => void;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
||||
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
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 { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor";
|
||||
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
||||
import { pdfExportService } from "../../services/pdfExportService";
|
||||
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||
@ -36,31 +36,7 @@ import { usePageDocument } from './hooks/usePageDocument';
|
||||
import { usePageEditorState } from './hooks/usePageEditorState';
|
||||
|
||||
export interface PageEditorProps {
|
||||
onFunctionsReady?: (functions: {
|
||||
handleUndo: () => void;
|
||||
handleRedo: () => void;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
handleRotate: (direction: 'left' | 'right') => void;
|
||||
handleDelete: () => void;
|
||||
handleSplit: () => void;
|
||||
handleSplitAll: () => void;
|
||||
handlePageBreak: () => void;
|
||||
handlePageBreakAll: () => void;
|
||||
handleSelectAll: () => void;
|
||||
handleDeselectAll: () => void;
|
||||
handleSetSelectedPages: (pageNumbers: number[]) => void;
|
||||
showExportPreview: (selectedOnly: boolean) => void;
|
||||
onExportSelected: () => void;
|
||||
onExportAll: () => void;
|
||||
applyChanges: () => void;
|
||||
exportLoading: boolean;
|
||||
selectionMode: boolean;
|
||||
selectedPages: number[];
|
||||
splitPositions: Set<number>;
|
||||
totalPages: number;
|
||||
closePdf: () => void;
|
||||
}) => void;
|
||||
onFunctionsReady?: (functions: PageEditorFunctions) => void;
|
||||
}
|
||||
|
||||
const PageEditor = ({
|
||||
@ -100,8 +76,8 @@ const PageEditor = ({
|
||||
|
||||
// UI state management
|
||||
const {
|
||||
selectionMode, selectedPageNumbers, movingPage, isAnimating, splitPositions, exportLoading,
|
||||
setSelectionMode, setSelectedPageNumbers, setMovingPage, setIsAnimating, setSplitPositions, setExportLoading,
|
||||
selectionMode, selectedPageIds, movingPage, isAnimating, splitPositions, exportLoading,
|
||||
setSelectionMode, setSelectedPageIds, setMovingPage, setIsAnimating, setSplitPositions, setExportLoading,
|
||||
togglePage, toggleSelectAll, animateReorder
|
||||
} = usePageEditorState();
|
||||
|
||||
@ -152,17 +128,40 @@ const PageEditor = ({
|
||||
|
||||
// Interface functions for parent component
|
||||
const displayDocument = editedDocument || mergedPdfDocument;
|
||||
|
||||
// Utility functions to convert between page IDs and page numbers
|
||||
const getPageNumbersFromIds = useCallback((pageIds: string[]): number[] => {
|
||||
if (!displayDocument) return [];
|
||||
return pageIds.map(id => {
|
||||
const page = displayDocument.pages.find(p => p.id === id);
|
||||
return page?.pageNumber || 0;
|
||||
}).filter(num => num > 0);
|
||||
}, [displayDocument]);
|
||||
|
||||
const getPageIdsFromNumbers = useCallback((pageNumbers: number[]): string[] => {
|
||||
if (!displayDocument) return [];
|
||||
return pageNumbers.map(num => {
|
||||
const page = displayDocument.pages.find(p => p.pageNumber === num);
|
||||
return page?.id || '';
|
||||
}).filter(id => id !== '');
|
||||
}, [displayDocument]);
|
||||
|
||||
// Convert selectedPageIds to numbers for components that still need numbers
|
||||
const selectedPageNumbers = useMemo(() =>
|
||||
getPageNumbersFromIds(selectedPageIds),
|
||||
[selectedPageIds, getPageNumbersFromIds]
|
||||
);
|
||||
|
||||
// Select all pages by default when document initially loads
|
||||
const hasInitializedSelection = useRef(false);
|
||||
useEffect(() => {
|
||||
if (displayDocument && displayDocument.pages.length > 0 && !hasInitializedSelection.current) {
|
||||
const allPageNumbers = Array.from({ length: displayDocument.pages.length }, (_, i) => i + 1);
|
||||
setSelectedPageNumbers(allPageNumbers);
|
||||
const allPageIds = displayDocument.pages.map(p => p.id);
|
||||
setSelectedPageIds(allPageIds);
|
||||
setSelectionMode(true);
|
||||
hasInitializedSelection.current = true;
|
||||
}
|
||||
}, [displayDocument, setSelectedPageNumbers, setSelectionMode]);
|
||||
}, [displayDocument, setSelectedPageIds, setSelectionMode]);
|
||||
|
||||
// DOM-first command handlers
|
||||
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
|
||||
@ -192,15 +191,18 @@ const PageEditor = ({
|
||||
pagesToDelete,
|
||||
() => displayDocument,
|
||||
setEditedDocument,
|
||||
setSelectedPageNumbers,
|
||||
(pageNumbers: number[]) => {
|
||||
const pageIds = getPageIdsFromNumbers(pageNumbers);
|
||||
setSelectedPageIds(pageIds);
|
||||
},
|
||||
() => splitPositions,
|
||||
setSplitPositions,
|
||||
() => selectedPageNumbers
|
||||
() => getPageNumbersFromIds(selectedPageIds)
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
}
|
||||
}
|
||||
}), [displayDocument, splitPositions, selectedPageNumbers]);
|
||||
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
||||
|
||||
const createSplitCommand = useCallback((position: number) => ({
|
||||
execute: () => {
|
||||
@ -230,30 +232,32 @@ const PageEditor = ({
|
||||
}, []);
|
||||
|
||||
const handleRotate = useCallback((direction: 'left' | 'right') => {
|
||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
const rotation = direction === 'left' ? -90 : 90;
|
||||
const pagesToRotate = selectedPageNumbers.map(pageNum => {
|
||||
const page = displayDocument.pages.find(p => p.pageNumber === pageNum);
|
||||
return page?.id || '';
|
||||
}).filter(id => id);
|
||||
|
||||
handleRotatePages(pagesToRotate, rotation);
|
||||
}, [displayDocument, selectedPageNumbers, handleRotatePages]);
|
||||
|
||||
handleRotatePages(selectedPageIds, rotation);
|
||||
}, [displayDocument, selectedPageIds, handleRotatePages]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
|
||||
// Convert selected page IDs to page numbers for the command
|
||||
const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds);
|
||||
|
||||
const deleteCommand = new DeletePagesCommand(
|
||||
selectedPageNumbers,
|
||||
() => displayDocument,
|
||||
setEditedDocument,
|
||||
setSelectedPageNumbers,
|
||||
(pageNumbers: number[]) => {
|
||||
const pageIds = getPageIdsFromNumbers(pageNumbers);
|
||||
setSelectedPageIds(pageIds);
|
||||
},
|
||||
() => splitPositions,
|
||||
setSplitPositions,
|
||||
() => selectedPageNumbers
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
}, [selectedPageNumbers, displayDocument, splitPositions]);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]);
|
||||
|
||||
const handleDeletePage = useCallback((pageNumber: number) => {
|
||||
if (!displayDocument) return;
|
||||
@ -262,18 +266,22 @@ const PageEditor = ({
|
||||
[pageNumber],
|
||||
() => displayDocument,
|
||||
setEditedDocument,
|
||||
setSelectedPageNumbers,
|
||||
(pageNumbers: number[]) => {
|
||||
const pageIds = getPageIdsFromNumbers(pageNumbers);
|
||||
setSelectedPageIds(pageIds);
|
||||
},
|
||||
() => splitPositions,
|
||||
setSplitPositions,
|
||||
() => selectedPageNumbers
|
||||
() => getPageNumbersFromIds(selectedPageIds)
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
}, [displayDocument, splitPositions, selectedPageNumbers]);
|
||||
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
||||
|
||||
const handleSplit = useCallback(() => {
|
||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
|
||||
// Convert selected page numbers to split positions (0-based indices)
|
||||
// Convert selected page IDs to page numbers, then to split positions (0-based indices)
|
||||
const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds);
|
||||
const selectedPositions: number[] = [];
|
||||
selectedPageNumbers.forEach(pageNum => {
|
||||
const pageIndex = displayDocument.pages.findIndex(p => p.pageNumber === pageNum);
|
||||
@ -314,12 +322,13 @@ const PageEditor = ({
|
||||
};
|
||||
|
||||
undoManagerRef.current.executeCommand(smartSplitCommand);
|
||||
}, [selectedPageNumbers, displayDocument, splitPositions, setSplitPositions]);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
|
||||
|
||||
const handleSplitAll = useCallback(() => {
|
||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
|
||||
// Convert selected page numbers to split positions (0-based indices)
|
||||
// Convert selected page IDs to page numbers, then to split positions (0-based indices)
|
||||
const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds);
|
||||
const selectedPositions: number[] = [];
|
||||
selectedPageNumbers.forEach(pageNum => {
|
||||
const pageIndex = displayDocument.pages.findIndex(p => p.pageNumber === pageNum);
|
||||
@ -359,31 +368,35 @@ const PageEditor = ({
|
||||
};
|
||||
|
||||
undoManagerRef.current.executeCommand(smartSplitCommand);
|
||||
}, [selectedPageNumbers, displayDocument, splitPositions, setSplitPositions]);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
|
||||
|
||||
const handlePageBreak = useCallback(() => {
|
||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
|
||||
// Convert selected page IDs to page numbers for the command
|
||||
const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds);
|
||||
|
||||
const pageBreakCommand = new PageBreakCommand(
|
||||
selectedPageNumbers,
|
||||
() => displayDocument,
|
||||
setEditedDocument,
|
||||
setSelectedPageNumbers
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
||||
}, [selectedPageNumbers, displayDocument]);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
|
||||
|
||||
const handlePageBreakAll = useCallback(() => {
|
||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
|
||||
// Convert selected page IDs to page numbers for the command
|
||||
const selectedPageNumbers = getPageNumbersFromIds(selectedPageIds);
|
||||
|
||||
const pageBreakCommand = new PageBreakCommand(
|
||||
selectedPageNumbers,
|
||||
() => displayDocument,
|
||||
setEditedDocument,
|
||||
setSelectedPageNumbers
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
||||
}, [selectedPageNumbers, displayDocument]);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
|
||||
|
||||
const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
|
||||
if (!displayDocument || files.length === 0) return;
|
||||
@ -400,21 +413,25 @@ const PageEditor = ({
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
if (!displayDocument) return;
|
||||
const allPageNumbers = Array.from({ length: displayDocument.pages.length }, (_, i) => i + 1);
|
||||
setSelectedPageNumbers(allPageNumbers);
|
||||
}, [displayDocument]);
|
||||
const allPageIds = displayDocument.pages.map(p => p.id);
|
||||
toggleSelectAll(allPageIds);
|
||||
}, [displayDocument, toggleSelectAll]);
|
||||
|
||||
const handleDeselectAll = useCallback(() => {
|
||||
setSelectedPageNumbers([]);
|
||||
}, []);
|
||||
setSelectedPageIds([]);
|
||||
}, [setSelectedPageIds]);
|
||||
|
||||
const handleSetSelectedPages = useCallback((pageNumbers: number[]) => {
|
||||
setSelectedPageNumbers(pageNumbers);
|
||||
}, []);
|
||||
const pageIds = getPageIdsFromNumbers(pageNumbers);
|
||||
setSelectedPageIds(pageIds);
|
||||
}, [getPageIdsFromNumbers, setSelectedPageIds]);
|
||||
|
||||
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => {
|
||||
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => {
|
||||
if (!displayDocument) return;
|
||||
|
||||
// Convert selectedPageIds to page numbers for the reorder command
|
||||
const selectedPages = selectedPageIds ? getPageNumbersFromIds(selectedPageIds) : undefined;
|
||||
|
||||
const reorderCommand = new ReorderPagesCommand(
|
||||
sourcePageNumber,
|
||||
targetIndex,
|
||||
@ -423,7 +440,7 @@ const PageEditor = ({
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(reorderCommand);
|
||||
}, [displayDocument]);
|
||||
}, [displayDocument, getPageNumbersFromIds]);
|
||||
|
||||
// Helper function to collect source files for multi-file export
|
||||
const getSourceFiles = useCallback((): Map<string, File> | null => {
|
||||
@ -466,7 +483,7 @@ const PageEditor = ({
|
||||
}, [activeFileIds, selectors, displayDocument]);
|
||||
|
||||
const onExportSelected = useCallback(async () => {
|
||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
|
||||
setExportLoading(true);
|
||||
try {
|
||||
@ -480,11 +497,11 @@ const PageEditor = ({
|
||||
// For selected pages export, we work with the first document (or single document)
|
||||
const documentWithDOMState = Array.isArray(processedDocuments) ? processedDocuments[0] : processedDocuments;
|
||||
|
||||
// Step 2: Convert selected page numbers to page IDs from the document with DOM state
|
||||
const selectedPageIds = selectedPageNumbers.map(pageNum => {
|
||||
const page = documentWithDOMState.pages.find(p => p.pageNumber === pageNum);
|
||||
return page?.id || '';
|
||||
}).filter(id => id);
|
||||
// Step 2: Use the already selected page IDs
|
||||
// Filter to only include IDs that exist in the document with DOM state
|
||||
const validSelectedPageIds = selectedPageIds.filter(pageId =>
|
||||
documentWithDOMState.pages.some(p => p.id === pageId)
|
||||
);
|
||||
|
||||
// Step 3: Export with pdfExportService
|
||||
|
||||
@ -494,12 +511,12 @@ const PageEditor = ({
|
||||
? await pdfExportService.exportPDFMultiFile(
|
||||
documentWithDOMState,
|
||||
sourceFiles,
|
||||
selectedPageIds,
|
||||
validSelectedPageIds,
|
||||
{ selectedOnly: true, filename: exportFilename }
|
||||
)
|
||||
: await pdfExportService.exportPDF(
|
||||
documentWithDOMState,
|
||||
selectedPageIds,
|
||||
validSelectedPageIds,
|
||||
{ selectedOnly: true, filename: exportFilename }
|
||||
);
|
||||
|
||||
@ -511,7 +528,7 @@ const PageEditor = ({
|
||||
console.error('Export failed:', error);
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [displayDocument, selectedPageNumbers, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
|
||||
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
|
||||
|
||||
const onExportAll = useCallback(async () => {
|
||||
if (!displayDocument) return;
|
||||
@ -606,7 +623,7 @@ const PageEditor = ({
|
||||
const closePdf = useCallback(() => {
|
||||
actions.clearAllFiles();
|
||||
undoManagerRef.current.clear();
|
||||
setSelectedPageNumbers([]);
|
||||
setSelectedPageIds([]);
|
||||
setSelectionMode(false);
|
||||
}, [actions]);
|
||||
|
||||
@ -646,7 +663,8 @@ const PageEditor = ({
|
||||
applyChanges,
|
||||
exportLoading,
|
||||
selectionMode,
|
||||
selectedPages: selectedPageNumbers,
|
||||
selectedPageIds,
|
||||
displayDocument: displayDocument || undefined,
|
||||
splitPositions,
|
||||
totalPages: displayDocument?.pages.length || 0,
|
||||
closePdf,
|
||||
@ -655,7 +673,7 @@ const PageEditor = ({
|
||||
}, [
|
||||
onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll,
|
||||
handlePageBreak, handlePageBreakAll, handleSelectAll, handleDeselectAll, handleSetSelectedPages, handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading,
|
||||
selectionMode, selectedPageNumbers, splitPositions, displayDocument?.pages.length, closePdf
|
||||
selectionMode, selectedPageIds, splitPositions, displayDocument?.pages.length, closePdf
|
||||
]);
|
||||
|
||||
// Display all pages - use edited or original document
|
||||
@ -745,7 +763,7 @@ const PageEditor = ({
|
||||
{/* Pages Grid */}
|
||||
<DragDropGrid
|
||||
items={displayedPages}
|
||||
selectedItems={selectedPageNumbers}
|
||||
selectedItems={selectedPageIds}
|
||||
selectionMode={selectionMode}
|
||||
isAnimating={isAnimating}
|
||||
onReorderPages={handleReorderPages}
|
||||
@ -756,7 +774,7 @@ const PageEditor = ({
|
||||
index={index}
|
||||
totalPages={displayDocument.pages.length}
|
||||
originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined}
|
||||
selectedPages={selectedPageNumbers}
|
||||
selectedPageIds={selectedPageIds}
|
||||
selectionMode={selectionMode}
|
||||
movingPage={movingPage}
|
||||
isAnimating={isAnimating}
|
||||
|
@ -37,7 +37,8 @@ interface PageEditorControlsProps {
|
||||
|
||||
// Selection state
|
||||
selectionMode: boolean;
|
||||
selectedPages: number[];
|
||||
selectedPageIds: string[];
|
||||
displayDocument?: { pages: { id: string; pageNumber: number }[] };
|
||||
|
||||
// Split state (for tooltip logic)
|
||||
splitPositions?: Set<number>;
|
||||
@ -59,18 +60,23 @@ const PageEditorControls = ({
|
||||
onExportAll,
|
||||
exportLoading,
|
||||
selectionMode,
|
||||
selectedPages,
|
||||
selectedPageIds,
|
||||
displayDocument,
|
||||
splitPositions,
|
||||
totalPages
|
||||
}: PageEditorControlsProps) => {
|
||||
// Calculate split tooltip text using smart toggle logic
|
||||
const getSplitTooltip = () => {
|
||||
if (!splitPositions || !totalPages || selectedPages.length === 0) {
|
||||
if (!splitPositions || !totalPages || selectedPageIds.length === 0) {
|
||||
return "Split Selected";
|
||||
}
|
||||
|
||||
// Convert selected pages to split positions (same logic as handleSplit)
|
||||
const selectedSplitPositions = selectedPages.map(pageNum => pageNum - 1).filter(pos => pos < totalPages - 1);
|
||||
const selectedPageNumbers = displayDocument ? selectedPageIds.map(id => {
|
||||
const page = displayDocument.pages.find(p => p.id === id);
|
||||
return page?.pageNumber || 0;
|
||||
}).filter(num => num > 0) : [];
|
||||
const selectedSplitPositions = selectedPageNumbers.map(pageNum => pageNum - 1).filter(pos => pos < totalPages - 1);
|
||||
|
||||
if (selectedSplitPositions.length === 0) {
|
||||
return "Split Selected";
|
||||
@ -97,8 +103,8 @@ const PageEditorControls = ({
|
||||
|
||||
// Calculate page break tooltip text
|
||||
const getPageBreakTooltip = () => {
|
||||
return selectedPages.length > 0
|
||||
? `Insert ${selectedPages.length} Page Break${selectedPages.length > 1 ? 's' : ''}`
|
||||
return selectedPageIds.length > 0
|
||||
? `Insert ${selectedPageIds.length} Page Break${selectedPageIds.length > 1 ? 's' : ''}`
|
||||
: "Insert Page Breaks";
|
||||
};
|
||||
|
||||
@ -157,7 +163,7 @@ const PageEditorControls = ({
|
||||
<Tooltip label="Rotate Selected Left">
|
||||
<ActionIcon
|
||||
onClick={() => onRotate('left')}
|
||||
disabled={selectedPages.length === 0}
|
||||
disabled={selectedPageIds.length === 0}
|
||||
variant="subtle"
|
||||
style={{ color: 'var(--mantine-color-dimmed)' }}
|
||||
radius="md"
|
||||
@ -169,7 +175,7 @@ const PageEditorControls = ({
|
||||
<Tooltip label="Rotate Selected Right">
|
||||
<ActionIcon
|
||||
onClick={() => onRotate('right')}
|
||||
disabled={selectedPages.length === 0}
|
||||
disabled={selectedPageIds.length === 0}
|
||||
variant="subtle"
|
||||
style={{ color: 'var(--mantine-color-dimmed)' }}
|
||||
radius="md"
|
||||
@ -181,7 +187,7 @@ const PageEditorControls = ({
|
||||
<Tooltip label="Delete Selected">
|
||||
<ActionIcon
|
||||
onClick={onDelete}
|
||||
disabled={selectedPages.length === 0}
|
||||
disabled={selectedPageIds.length === 0}
|
||||
variant="subtle"
|
||||
style={{ color: 'var(--mantine-color-dimmed)' }}
|
||||
radius="md"
|
||||
@ -193,7 +199,7 @@ const PageEditorControls = ({
|
||||
<Tooltip label={getSplitTooltip()}>
|
||||
<ActionIcon
|
||||
onClick={onSplit}
|
||||
disabled={selectedPages.length === 0}
|
||||
disabled={selectedPageIds.length === 0}
|
||||
variant="subtle"
|
||||
style={{ color: 'var(--mantine-color-dimmed)' }}
|
||||
radius="md"
|
||||
@ -205,7 +211,7 @@ const PageEditorControls = ({
|
||||
<Tooltip label={getPageBreakTooltip()}>
|
||||
<ActionIcon
|
||||
onClick={onPageBreak}
|
||||
disabled={selectedPages.length === 0}
|
||||
disabled={selectedPageIds.length === 0}
|
||||
variant="subtle"
|
||||
style={{ color: 'var(--mantine-color-dimmed)' }}
|
||||
radius="md"
|
||||
|
@ -19,13 +19,13 @@ interface PageThumbnailProps {
|
||||
index: number;
|
||||
totalPages: number;
|
||||
originalFile?: File;
|
||||
selectedPages: number[];
|
||||
selectedPageIds: string[];
|
||||
selectionMode: boolean;
|
||||
movingPage: number | null;
|
||||
isAnimating: boolean;
|
||||
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => void;
|
||||
onTogglePage: (pageNumber: number) => void;
|
||||
onReorderPages: (sourcePageNumber: number, targetIndex: number, selectedPageIds?: string[]) => void;
|
||||
onTogglePage: (pageId: string) => void;
|
||||
onAnimateReorder: () => void;
|
||||
onExecuteCommand: (command: { execute: () => void }) => void;
|
||||
onSetStatus: (status: string) => void;
|
||||
@ -45,7 +45,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
index,
|
||||
totalPages,
|
||||
originalFile,
|
||||
selectedPages,
|
||||
selectedPageIds,
|
||||
selectionMode,
|
||||
movingPage,
|
||||
isAnimating,
|
||||
@ -139,7 +139,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
getInitialData: () => ({
|
||||
pageNumber: page.pageNumber,
|
||||
pageId: page.id,
|
||||
selectedPages: [page.pageNumber]
|
||||
selectedPageIds: [page.id]
|
||||
}),
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
@ -185,7 +185,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
(dragElementRef.current as any).__dragCleanup();
|
||||
}
|
||||
}
|
||||
}, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPages, pdfDocument.pages, onReorderPages]);
|
||||
}, [page.id, page.pageNumber, pageRefs, selectionMode, selectedPageIds, pdfDocument.pages, onReorderPages]);
|
||||
|
||||
// DOM command handlers
|
||||
const handleRotateLeft = useCallback((e: React.MouseEvent) => {
|
||||
@ -263,12 +263,12 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
|
||||
// If mouse moved less than 5 pixels, consider it a click (not a drag)
|
||||
if (distance < 5 && !isDragging) {
|
||||
onTogglePage(page.pageNumber);
|
||||
onTogglePage(page.id);
|
||||
}
|
||||
|
||||
setIsMouseDown(false);
|
||||
setMouseStartPos(null);
|
||||
}, [isMouseDown, mouseStartPos, isDragging, page.pageNumber, onTogglePage]);
|
||||
}, [isMouseDown, mouseStartPos, isDragging, page.id, onTogglePage]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
setIsMouseDown(false);
|
||||
@ -323,7 +323,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
onTogglePage(page.pageNumber);
|
||||
onTogglePage(page.id);
|
||||
}}
|
||||
onMouseUp={(e) => e.stopPropagation()}
|
||||
onDragStart={(e) => {
|
||||
@ -332,7 +332,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={Array.isArray(selectedPages) ? selectedPages.includes(page.pageNumber) : false}
|
||||
checked={Array.isArray(selectedPageIds) ? selectedPageIds.includes(page.id) : false}
|
||||
onChange={() => {
|
||||
// Selection is handled by container mouseDown
|
||||
}}
|
||||
|
@ -403,8 +403,7 @@ export class PageBreakCommand extends DOMCommand {
|
||||
constructor(
|
||||
private selectedPageNumbers: number[],
|
||||
private getCurrentDocument: () => PDFDocument | null,
|
||||
private setDocument: (doc: PDFDocument) => void,
|
||||
private setSelectedPages: (pages: number[]) => void
|
||||
private setDocument: (doc: PDFDocument) => void
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@ -455,19 +454,7 @@ export class PageBreakCommand extends DOMCommand {
|
||||
|
||||
this.setDocument(updatedDocument);
|
||||
|
||||
// Maintain existing selection by mapping original selected pages to their new positions
|
||||
const updatedSelection: number[] = [];
|
||||
this.selectedPageNumbers.forEach(originalPageNum => {
|
||||
// Find the original page by matching the page ID from the original document
|
||||
const originalPage = this.originalDocument?.pages[originalPageNum - 1];
|
||||
if (originalPage) {
|
||||
const foundPage = newPages.find(page => page.id === originalPage.id && !page.isBlankPage);
|
||||
if (foundPage) {
|
||||
updatedSelection.push(foundPage.pageNumber);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.setSelectedPages(updatedSelection);
|
||||
// No need to maintain selection - page IDs remain stable, so selection persists automatically
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
|
@ -3,7 +3,7 @@ import { useState, useCallback } from 'react';
|
||||
export interface PageEditorState {
|
||||
// Selection state
|
||||
selectionMode: boolean;
|
||||
selectedPageNumbers: number[];
|
||||
selectedPageIds: string[];
|
||||
|
||||
// Animation state
|
||||
movingPage: number | null;
|
||||
@ -17,15 +17,15 @@ export interface PageEditorState {
|
||||
|
||||
// Actions
|
||||
setSelectionMode: (mode: boolean) => void;
|
||||
setSelectedPageNumbers: (pages: number[]) => void;
|
||||
setSelectedPageIds: (pages: string[]) => void;
|
||||
setMovingPage: (pageNumber: number | null) => void;
|
||||
setIsAnimating: (animating: boolean) => void;
|
||||
setSplitPositions: (positions: Set<number>) => void;
|
||||
setExportLoading: (loading: boolean) => void;
|
||||
|
||||
// Helper functions
|
||||
togglePage: (pageNumber: number) => void;
|
||||
toggleSelectAll: (totalPages: number) => void;
|
||||
togglePage: (pageId: string) => void;
|
||||
toggleSelectAll: (allPageIds: string[]) => void;
|
||||
animateReorder: () => void;
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ export interface PageEditorState {
|
||||
export function usePageEditorState(): PageEditorState {
|
||||
// Selection state
|
||||
const [selectionMode, setSelectionMode] = useState(false);
|
||||
const [selectedPageNumbers, setSelectedPageNumbers] = useState<number[]>([]);
|
||||
const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
|
||||
|
||||
// Animation state
|
||||
const [movingPage, setMovingPage] = useState<number | null>(null);
|
||||
@ -49,20 +49,19 @@ export function usePageEditorState(): PageEditorState {
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
|
||||
// Helper functions
|
||||
const togglePage = useCallback((pageNumber: number) => {
|
||||
setSelectedPageNumbers(prev =>
|
||||
prev.includes(pageNumber)
|
||||
? prev.filter(n => n !== pageNumber)
|
||||
: [...prev, pageNumber]
|
||||
const togglePage = useCallback((pageId: string) => {
|
||||
setSelectedPageIds(prev =>
|
||||
prev.includes(pageId)
|
||||
? prev.filter(id => id !== pageId)
|
||||
: [...prev, pageId]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const toggleSelectAll = useCallback((totalPages: number) => {
|
||||
if (!totalPages) return;
|
||||
const toggleSelectAll = useCallback((allPageIds: string[]) => {
|
||||
if (!allPageIds.length) return;
|
||||
|
||||
const allPageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
setSelectedPageNumbers(prev =>
|
||||
prev.length === allPageNumbers.length ? [] : allPageNumbers
|
||||
setSelectedPageIds(prev =>
|
||||
prev.length === allPageIds.length ? [] : allPageIds
|
||||
);
|
||||
}, []);
|
||||
|
||||
@ -74,7 +73,7 @@ export function usePageEditorState(): PageEditorState {
|
||||
return {
|
||||
// State
|
||||
selectionMode,
|
||||
selectedPageNumbers,
|
||||
selectedPageIds,
|
||||
movingPage,
|
||||
isAnimating,
|
||||
splitPositions,
|
||||
@ -82,7 +81,7 @@ export function usePageEditorState(): PageEditorState {
|
||||
|
||||
// Setters
|
||||
setSelectionMode,
|
||||
setSelectedPageNumbers,
|
||||
setSelectedPageIds,
|
||||
setMovingPage,
|
||||
setIsAnimating,
|
||||
setSplitPositions,
|
||||
|
@ -47,7 +47,7 @@ export default function RightRail() {
|
||||
if (currentView === 'pageEditor') {
|
||||
// Use PageEditor's own state
|
||||
const totalItems = pageEditorFunctions?.totalPages || 0;
|
||||
const selectedCount = pageEditorFunctions?.selectedPages?.length || 0;
|
||||
const selectedCount = pageEditorFunctions?.selectedPageIds?.length || 0;
|
||||
return { totalItems, selectedCount };
|
||||
}
|
||||
|
||||
@ -147,12 +147,15 @@ export default function RightRail() {
|
||||
|
||||
// Sync csvInput with PageEditor's selected pages
|
||||
useEffect(() => {
|
||||
const sortedPageNumbers = Array.isArray(pageEditorFunctions?.selectedPages)
|
||||
? [...pageEditorFunctions.selectedPages].sort((a, b) => a - b)
|
||||
const sortedPageNumbers = Array.isArray(pageEditorFunctions?.selectedPageIds) && pageEditorFunctions.displayDocument
|
||||
? pageEditorFunctions.selectedPageIds.map(id => {
|
||||
const page = pageEditorFunctions.displayDocument!.pages.find(p => p.id === id);
|
||||
return page?.pageNumber || 0;
|
||||
}).filter(num => num > 0).sort((a, b) => a - b)
|
||||
: [];
|
||||
const newCsvInput = sortedPageNumbers.join(', ');
|
||||
setCsvInput(newCsvInput);
|
||||
}, [pageEditorFunctions?.selectedPages]);
|
||||
}, [pageEditorFunctions?.selectedPageIds]);
|
||||
|
||||
// Clear CSV input when files change (use stable signature to avoid ref churn)
|
||||
useEffect(() => {
|
||||
@ -262,7 +265,8 @@ export default function RightRail() {
|
||||
<BulkSelectionPanel
|
||||
csvInput={csvInput}
|
||||
setCsvInput={setCsvInput}
|
||||
selectedPages={Array.isArray(pageEditorFunctions?.selectedPages) ? pageEditorFunctions.selectedPages : []}
|
||||
selectedPageIds={Array.isArray(pageEditorFunctions?.selectedPageIds) ? pageEditorFunctions.selectedPageIds : []}
|
||||
displayDocument={pageEditorFunctions?.displayDocument}
|
||||
onUpdatePagesFromCSV={updatePagesFromCSV}
|
||||
/>
|
||||
</div>
|
||||
@ -284,7 +288,7 @@ export default function RightRail() {
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => { pageEditorFunctions?.handleDelete?.(); }}
|
||||
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPages?.length || 0) === 0}
|
||||
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length || 0) === 0}
|
||||
aria-label={typeof t === 'function' ? t('rightRail.deleteSelected', 'Delete Selected Pages') : 'Delete Selected Pages'}
|
||||
>
|
||||
<LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />
|
||||
@ -305,7 +309,7 @@ export default function RightRail() {
|
||||
radius="md"
|
||||
className="right-rail-icon"
|
||||
onClick={() => { pageEditorFunctions?.onExportSelected?.(); }}
|
||||
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPages?.length || 0) === 0 || pageEditorFunctions?.exportLoading}
|
||||
disabled={!pageControlsVisible || (pageEditorFunctions?.selectedPageIds?.length || 0) === 0 || pageEditorFunctions?.exportLoading}
|
||||
aria-label={typeof t === 'function' ? t('rightRail.exportSelected', 'Export Selected Pages') : 'Export Selected Pages'}
|
||||
>
|
||||
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
|
||||
|
@ -11,8 +11,6 @@ interface RainbowThemeHook {
|
||||
deactivateRainbow: () => void;
|
||||
}
|
||||
|
||||
const allowRainbowMode = false; // Override to allow/disallow fun
|
||||
|
||||
export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): RainbowThemeHook {
|
||||
// Get theme from localStorage or use initial
|
||||
const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
|
||||
@ -37,11 +35,11 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
|
||||
// Save theme to localStorage whenever it changes
|
||||
useEffect(() => {
|
||||
localStorage.setItem('stirling-theme', themeMode);
|
||||
|
||||
|
||||
// Apply rainbow class to body if in rainbow mode
|
||||
if (themeMode === 'rainbow') {
|
||||
document.body.classList.add('rainbow-mode-active');
|
||||
|
||||
|
||||
// Show easter egg notification
|
||||
showRainbowNotification();
|
||||
} else {
|
||||
@ -79,7 +77,7 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
`;
|
||||
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Auto-remove notification after 3 seconds
|
||||
@ -123,7 +121,7 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease;
|
||||
`;
|
||||
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Auto-remove notification after 2 seconds
|
||||
@ -146,7 +144,7 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
|
||||
}
|
||||
|
||||
const currentTime = Date.now();
|
||||
|
||||
|
||||
// Simple exit from rainbow mode with single click (after cooldown period)
|
||||
if (themeMode === 'rainbow') {
|
||||
setThemeMode('light');
|
||||
@ -154,7 +152,7 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
|
||||
showExitNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Reset counter if too much time has passed (2.5 seconds)
|
||||
if (currentTime - lastToggleTime.current > 2500) {
|
||||
toggleCount.current = 1;
|
||||
@ -164,18 +162,18 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
|
||||
lastToggleTime.current = currentTime;
|
||||
|
||||
// Easter egg: Activate rainbow mode after 10 rapid toggles
|
||||
if (allowRainbowMode && toggleCount.current >= 10) {
|
||||
if (toggleCount.current >= 10) {
|
||||
setThemeMode('rainbow');
|
||||
console.log('🌈 RAINBOW MODE ACTIVATED! 🌈 You found the secret easter egg!');
|
||||
console.log('🌈 Button will be disabled for 3 seconds, then click once to exit!');
|
||||
|
||||
|
||||
// Disable toggle for 3 seconds
|
||||
setIsToggleDisabled(true);
|
||||
setTimeout(() => {
|
||||
setIsToggleDisabled(false);
|
||||
console.log('🌈 Theme toggle re-enabled! Click once to exit rainbow mode.');
|
||||
}, 3000);
|
||||
|
||||
|
||||
// Reset counter
|
||||
toggleCount.current = 0;
|
||||
return;
|
||||
@ -205,4 +203,4 @@ export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): Rainb
|
||||
activateRainbow,
|
||||
deactivateRainbow,
|
||||
};
|
||||
}
|
||||
}
|
@ -50,9 +50,9 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
compressOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
actions.setMode("compress");
|
||||
};
|
||||
onPreviewFile?.(null); };
|
||||
|
||||
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = compressOperation.files.length > 0 || compressOperation.downloadUrl !== null;
|
||||
|
@ -28,13 +28,14 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
onPreviewFile?.(null);
|
||||
}, [splitParams.parameters]);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(() => {
|
||||
// Reset results when selected files change (user selected different files)
|
||||
if (selectedFiles.length > 0) {
|
||||
splitOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
}
|
||||
}, [selectedFiles]);
|
||||
|
||||
const handleSplit = async () => {
|
||||
try {
|
||||
await splitOperation.executeOperation(splitParams.parameters, selectedFiles);
|
||||
@ -56,8 +57,7 @@ const Split = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const handleSettingsReset = () => {
|
||||
splitOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
actions.setMode("split");
|
||||
};
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
const hasResults = splitOperation.files.length > 0 || splitOperation.downloadUrl !== null;
|
||||
|
@ -56,11 +56,14 @@ export interface PageEditorFunctions {
|
||||
handleSelectAll: () => void;
|
||||
handleDeselectAll: () => void;
|
||||
handleSetSelectedPages: (pageNumbers: number[]) => void;
|
||||
showExportPreview: (selectedOnly: boolean) => void;
|
||||
onExportSelected: () => void;
|
||||
onExportAll: () => void;
|
||||
applyChanges: () => void;
|
||||
exportLoading: boolean;
|
||||
selectionMode: boolean;
|
||||
selectedPages: number[];
|
||||
selectedPageIds: string[];
|
||||
displayDocument?: PDFDocument;
|
||||
splitPositions: Set<number>;
|
||||
totalPages: number;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user