mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
Update Page editor styling
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from "react";
|
||||
import {
|
||||
Button, Text, Center, Checkbox, Box, Tooltip, ActionIcon,
|
||||
Button, Text, Center, Checkbox, Box, Tooltip, ActionIcon,
|
||||
Notification, TextInput, FileInput, LoadingOverlay, Modal, Alert, Container,
|
||||
Stack, Group, Paper, SimpleGrid
|
||||
} from "@mantine/core";
|
||||
@@ -25,11 +25,11 @@ import { PDFDocument, PDFPage } from "../types/pageEditor";
|
||||
import { fileStorage } from "../services/fileStorage";
|
||||
import { generateThumbnailForFile } from "../utils/thumbnailUtils";
|
||||
import { useUndoRedo } from "../hooks/useUndoRedo";
|
||||
import {
|
||||
RotatePagesCommand,
|
||||
DeletePagesCommand,
|
||||
ReorderPageCommand,
|
||||
ToggleSplitCommand
|
||||
import {
|
||||
RotatePagesCommand,
|
||||
DeletePagesCommand,
|
||||
ReorderPageCommand,
|
||||
ToggleSplitCommand
|
||||
} from "../commands/pageCommands";
|
||||
import { pdfExportService } from "../services/pdfExportService";
|
||||
|
||||
@@ -48,7 +48,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { processPDFFile, loading: pdfLoading } = usePDFProcessor();
|
||||
|
||||
|
||||
const [pdfDocument, setPdfDocument] = useState<PDFDocument | null>(null);
|
||||
const [selectedPages, setSelectedPages] = useState<string[]>([]);
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
@@ -58,11 +58,12 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
const [showPageSelect, setShowPageSelect] = useState(false);
|
||||
const [filename, setFilename] = useState<string>("");
|
||||
const [draggedPage, setDraggedPage] = useState<string | null>(null);
|
||||
const [dropTarget, setDropTarget] = useState<string | null>(null);
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
const [showExportModal, setShowExportModal] = useState(false);
|
||||
const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null);
|
||||
const fileInputRef = useRef<() => void>(null);
|
||||
|
||||
|
||||
// Undo/Redo system
|
||||
const { executeCommand, undo, redo, canUndo, canRedo } = useUndoRedo();
|
||||
|
||||
@@ -75,23 +76,23 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
|
||||
try {
|
||||
const document = await processPDFFile(uploadedFile);
|
||||
setPdfDocument(document);
|
||||
setFilename(uploadedFile.name.replace(/\.pdf$/i, ''));
|
||||
setSelectedPages([]);
|
||||
|
||||
|
||||
if (document.pages.length > 0) {
|
||||
const thumbnail = await generateThumbnailForFile(uploadedFile);
|
||||
await fileStorage.storeFile(uploadedFile, thumbnail);
|
||||
}
|
||||
|
||||
|
||||
if (setFile) {
|
||||
const fileUrl = URL.createObjectURL(uploadedFile);
|
||||
setFile({ file: uploadedFile, url: fileUrl });
|
||||
}
|
||||
|
||||
|
||||
setStatus(`PDF loaded successfully with ${document.totalPages} pages`);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to process PDF';
|
||||
@@ -113,23 +114,23 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
setSelectedPages(pdfDocument.pages.map(p => p.id));
|
||||
}
|
||||
}, [pdfDocument]);
|
||||
|
||||
|
||||
const deselectAll = useCallback(() => setSelectedPages([]), []);
|
||||
|
||||
|
||||
const togglePage = useCallback((pageId: string) => {
|
||||
setSelectedPages(prev =>
|
||||
prev.includes(pageId)
|
||||
? prev.filter(id => id !== pageId)
|
||||
setSelectedPages(prev =>
|
||||
prev.includes(pageId)
|
||||
? prev.filter(id => id !== pageId)
|
||||
: [...prev, pageId]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const parseCSVInput = useCallback((csv: string) => {
|
||||
if (!pdfDocument) return [];
|
||||
|
||||
|
||||
const pageIds: string[] = [];
|
||||
const ranges = csv.split(',').map(s => s.trim()).filter(Boolean);
|
||||
|
||||
|
||||
ranges.forEach(range => {
|
||||
if (range.includes('-')) {
|
||||
const [start, end] = range.split('-').map(n => parseInt(n.trim()));
|
||||
@@ -147,7 +148,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return pageIds;
|
||||
}, [pdfDocument]);
|
||||
|
||||
@@ -162,14 +163,55 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!draggedPage) return;
|
||||
|
||||
// Get the element under the mouse cursor
|
||||
const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY);
|
||||
if (!elementUnderCursor) return;
|
||||
|
||||
// Find the closest page container
|
||||
const pageContainer = elementUnderCursor.closest('[data-page-id]');
|
||||
if (pageContainer) {
|
||||
const pageId = pageContainer.getAttribute('data-page-id');
|
||||
if (pageId && pageId !== draggedPage) {
|
||||
setDropTarget(pageId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if over the end zone
|
||||
const endZone = elementUnderCursor.closest('[data-drop-zone="end"]');
|
||||
if (endZone) {
|
||||
setDropTarget('end');
|
||||
return;
|
||||
}
|
||||
|
||||
// If not over any valid drop target, clear it
|
||||
setDropTarget(null);
|
||||
}, [draggedPage]);
|
||||
|
||||
const handleDragEnter = useCallback((pageId: string) => {
|
||||
if (draggedPage && pageId !== draggedPage) {
|
||||
setDropTarget(pageId);
|
||||
}
|
||||
}, [draggedPage]);
|
||||
|
||||
const handleDragLeave = useCallback(() => {
|
||||
// Don't clear drop target on drag leave - let dragover handle it
|
||||
}, []);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent, targetPageId: string) => {
|
||||
const handleDrop = useCallback((e: React.DragEvent, targetPageId: string | 'end') => {
|
||||
e.preventDefault();
|
||||
if (!draggedPage || !pdfDocument || draggedPage === targetPageId) return;
|
||||
|
||||
const targetIndex = pdfDocument.pages.findIndex(p => p.id === targetPageId);
|
||||
if (targetIndex === -1) return;
|
||||
let targetIndex: number;
|
||||
if (targetPageId === 'end') {
|
||||
targetIndex = pdfDocument.pages.length;
|
||||
} else {
|
||||
targetIndex = pdfDocument.pages.findIndex(p => p.id === targetPageId);
|
||||
if (targetIndex === -1) return;
|
||||
}
|
||||
|
||||
const command = new ReorderPageCommand(
|
||||
pdfDocument,
|
||||
@@ -177,15 +219,22 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
draggedPage,
|
||||
targetIndex
|
||||
);
|
||||
|
||||
|
||||
executeCommand(command);
|
||||
setDraggedPage(null);
|
||||
setDropTarget(null);
|
||||
setStatus('Page reordered');
|
||||
}, [draggedPage, pdfDocument, executeCommand]);
|
||||
|
||||
const handleEndZoneDragEnter = useCallback(() => {
|
||||
if (draggedPage) {
|
||||
setDropTarget('end');
|
||||
}
|
||||
}, [draggedPage]);
|
||||
|
||||
const handleRotate = useCallback((direction: 'left' | 'right') => {
|
||||
if (!pdfDocument || selectedPages.length === 0) return;
|
||||
|
||||
|
||||
const rotation = direction === 'left' ? -90 : 90;
|
||||
const command = new RotatePagesCommand(
|
||||
pdfDocument,
|
||||
@@ -193,20 +242,20 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
selectedPages,
|
||||
rotation
|
||||
);
|
||||
|
||||
|
||||
executeCommand(command);
|
||||
setStatus(`Rotated ${selectedPages.length} pages ${direction}`);
|
||||
}, [pdfDocument, selectedPages, executeCommand]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!pdfDocument || selectedPages.length === 0) return;
|
||||
|
||||
|
||||
const command = new DeletePagesCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
selectedPages
|
||||
);
|
||||
|
||||
|
||||
executeCommand(command);
|
||||
setSelectedPages([]);
|
||||
setStatus(`Deleted ${selectedPages.length} pages`);
|
||||
@@ -214,20 +263,20 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
|
||||
const handleSplit = useCallback(() => {
|
||||
if (!pdfDocument || selectedPages.length === 0) return;
|
||||
|
||||
|
||||
const command = new ToggleSplitCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
selectedPages
|
||||
);
|
||||
|
||||
|
||||
executeCommand(command);
|
||||
setStatus(`Split markers toggled for ${selectedPages.length} pages`);
|
||||
}, [pdfDocument, selectedPages, executeCommand]);
|
||||
|
||||
const showExportPreview = useCallback((selectedOnly: boolean = false) => {
|
||||
if (!pdfDocument) return;
|
||||
|
||||
|
||||
const exportPageIds = selectedOnly ? selectedPages : [];
|
||||
const preview = pdfExportService.getExportInfo(pdfDocument, exportPageIds, selectedOnly);
|
||||
setExportPreview(preview);
|
||||
@@ -236,7 +285,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
|
||||
const handleExport = useCallback(async (selectedOnly: boolean = false) => {
|
||||
if (!pdfDocument) return;
|
||||
|
||||
|
||||
setExportLoading(true);
|
||||
try {
|
||||
const exportPageIds = selectedOnly ? selectedPages : [];
|
||||
@@ -247,27 +296,27 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
}
|
||||
|
||||
const hasSplitMarkers = pdfDocument.pages.some(page => page.splitBefore);
|
||||
|
||||
|
||||
if (hasSplitMarkers) {
|
||||
const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, {
|
||||
selectedOnly,
|
||||
filename,
|
||||
splitDocuments: true
|
||||
}) as { blobs: Blob[]; filenames: string[] };
|
||||
|
||||
|
||||
result.blobs.forEach((blob, index) => {
|
||||
setTimeout(() => {
|
||||
pdfExportService.downloadFile(blob, result.filenames[index]);
|
||||
}, index * 500);
|
||||
});
|
||||
|
||||
|
||||
setStatus(`Exported ${result.blobs.length} split documents`);
|
||||
} else {
|
||||
const result = await pdfExportService.exportPDF(pdfDocument, exportPageIds, {
|
||||
selectedOnly,
|
||||
filename
|
||||
}) as { blob: Blob; filename: string };
|
||||
|
||||
|
||||
pdfExportService.downloadFile(result.blob, result.filename);
|
||||
setStatus('PDF exported successfully');
|
||||
}
|
||||
@@ -293,67 +342,82 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
|
||||
if (!pdfDocument) {
|
||||
return (
|
||||
<Container>
|
||||
<Paper shadow="xs" radius="md" p="md" pos="relative">
|
||||
<LoadingOverlay visible={loading || pdfLoading} />
|
||||
|
||||
<Group mb="md">
|
||||
<ConstructionIcon />
|
||||
<Text size="lg" fw={600}>PDF Multitool</Text>
|
||||
</Group>
|
||||
|
||||
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
|
||||
<LoadingOverlay visible={loading || pdfLoading} />
|
||||
|
||||
<Box p="xl">
|
||||
{error && (
|
||||
<Alert color="red" mb="md" onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
<Dropzone
|
||||
onDrop={(files) => files[0] && handleFileUpload(files[0])}
|
||||
accept={["application/pdf"]}
|
||||
multiple={false}
|
||||
h={300}
|
||||
h="60vh"
|
||||
style={{ minHeight: 400 }}
|
||||
>
|
||||
<Center h={250}>
|
||||
<Center h="100%">
|
||||
<Stack align="center" gap="md">
|
||||
<UploadFileIcon style={{ fontSize: 48 }} />
|
||||
<Text size="lg" fw={500}>
|
||||
<UploadFileIcon style={{ fontSize: 64 }} />
|
||||
<Text size="xl" fw={500}>
|
||||
Drop a PDF file here or click to upload
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
<Text size="md" c="dimmed">
|
||||
Supports PDF files only
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Dropzone>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Paper shadow="xs" radius="md" p="md" pos="relative">
|
||||
<LoadingOverlay visible={loading || pdfLoading} />
|
||||
|
||||
<Group mb="md">
|
||||
<ConstructionIcon />
|
||||
<Text size="lg" fw={600}>PDF Multitool</Text>
|
||||
</Group>
|
||||
|
||||
<Group mb="md">
|
||||
<TextInput
|
||||
value={filename}
|
||||
onChange={(e) => setFilename(e.target.value)}
|
||||
placeholder="Enter filename"
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
<Button onClick={() => setShowPageSelect(!showPageSelect)}>
|
||||
Select Pages
|
||||
</Button>
|
||||
<Button onClick={selectAll}>Select All</Button>
|
||||
<Button onClick={deselectAll}>Deselect All</Button>
|
||||
</Group>
|
||||
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
|
||||
<style>
|
||||
{`
|
||||
.page-container:hover .page-number {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.page-container:hover .page-hover-controls {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.page-container {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
.page-container:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<LoadingOverlay visible={loading || pdfLoading} />
|
||||
|
||||
<Box p="md">
|
||||
<Group mb="md">
|
||||
<TextInput
|
||||
value={filename}
|
||||
onChange={(e) => setFilename(e.target.value)}
|
||||
placeholder="Enter filename"
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
<Button onClick={() => setShowPageSelect(!showPageSelect)}>
|
||||
Select Pages
|
||||
</Button>
|
||||
<Button onClick={selectAll}>Select All</Button>
|
||||
<Button onClick={deselectAll}>Deselect All</Button>
|
||||
</Group>
|
||||
|
||||
{showPageSelect && (
|
||||
<Paper p="md" mb="md" withBorder>
|
||||
@@ -412,121 +476,308 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 2, sm: 3, md: 4, lg: 6 }} spacing="md">
|
||||
{pdfDocument.pages.map((page) => (
|
||||
<Box
|
||||
key={page.id}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
padding: 8,
|
||||
position: 'relative',
|
||||
cursor: 'grab',
|
||||
...(selectedPages.includes(page.id)
|
||||
? { border: '2px solid blue' }
|
||||
: { border: '1px solid #ccc' }
|
||||
),
|
||||
...(page.splitBefore
|
||||
? { borderLeft: '4px dashed orange' }
|
||||
: {}
|
||||
)
|
||||
}}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '1.5rem',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
{pdfDocument.pages.map((page, index) => (
|
||||
<React.Fragment key={page.id}>
|
||||
{page.splitBefore && index > 0 && (
|
||||
<div
|
||||
style={{
|
||||
width: '4px',
|
||||
height: '15rem',
|
||||
border: '2px dashed #3b82f6',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: '2px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '-0.75rem',
|
||||
marginRight: '-0.75rem',
|
||||
position: 'relative',
|
||||
flexShrink: 0
|
||||
}}
|
||||
>
|
||||
<ContentCutIcon
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: '#3b82f6',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '50%',
|
||||
padding: '3px'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
data-page-id={page.id}
|
||||
className={`
|
||||
!rounded-lg
|
||||
cursor-grab
|
||||
select-none
|
||||
w-[15rem]
|
||||
h-[15rem]
|
||||
flex items-center justify-center
|
||||
flex-shrink-0
|
||||
shadow-sm
|
||||
hover:shadow-md
|
||||
transition-all
|
||||
relative
|
||||
${selectedPages.includes(page.id)
|
||||
? 'ring-2 ring-blue-500 bg-blue-50'
|
||||
: 'bg-white hover:bg-gray-50'}
|
||||
${draggedPage === page.id ? 'opacity-50 scale-95' : ''}
|
||||
`}
|
||||
style={{
|
||||
transform: (() => {
|
||||
if (!draggedPage || page.id === draggedPage) return 'translateX(0)';
|
||||
|
||||
if (dropTarget === page.id) {
|
||||
return 'translateX(20px)'; // Move slightly right to indicate drop position
|
||||
}
|
||||
return 'translateX(0)';
|
||||
})(),
|
||||
transition: 'transform 0.2s ease-in-out'
|
||||
}}
|
||||
draggable
|
||||
onDragStart={() => handleDragStart(page.id)}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={() => handleDragEnter(page.id)}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, page.id)}
|
||||
>
|
||||
<Stack align="center" gap={4}>
|
||||
{showPageSelect && (
|
||||
<Checkbox
|
||||
checked={selectedPages.includes(page.id)}
|
||||
onChange={() => togglePage(page.id)}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box w={120} h={160} pos="relative">
|
||||
<img
|
||||
src={page.thumbnail}
|
||||
alt={`Page ${page.pageNumber}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
borderRadius: 4,
|
||||
transform: `rotate(${page.rotation}deg)`
|
||||
}}
|
||||
/>
|
||||
|
||||
<Text
|
||||
size="xs"
|
||||
fw={500}
|
||||
c="white"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
left: 4,
|
||||
background: 'rgba(0,0,0,0.7)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 4
|
||||
}}
|
||||
>
|
||||
{page.pageNumber}
|
||||
</Text>
|
||||
|
||||
<DragIndicatorIcon
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
color: 'rgba(0,0,0,0.5)',
|
||||
fontSize: 16
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
Page {page.pageNumber}
|
||||
<div className="page-container w-[90%] h-[90%]">
|
||||
<img
|
||||
src={page.thumbnail}
|
||||
alt={`Page ${page.pageNumber}`}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
borderRadius: 4,
|
||||
transform: `rotate(${page.rotation}deg)`,
|
||||
transition: 'transform 0.3s ease-in-out'
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Page number overlay - shows on hover */}
|
||||
<Text
|
||||
className="page-number"
|
||||
size="sm"
|
||||
fw={500}
|
||||
c="white"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
left: 5,
|
||||
background: 'rgba(162, 201, 255, 0.8)',
|
||||
padding: '6px 8px',
|
||||
borderRadius: 8,
|
||||
zIndex: 2,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s ease-in-out'
|
||||
}}
|
||||
>
|
||||
{page.pageNumber}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Hover controls */}
|
||||
<div
|
||||
className="page-hover-controls"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 8,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: '6px 12px',
|
||||
borderRadius: 20,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
zIndex: 3,
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Rotate Left">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const command = new RotatePagesCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
[page.id],
|
||||
-90
|
||||
);
|
||||
executeCommand(command);
|
||||
setStatus(`Rotated page ${page.pageNumber} left`);
|
||||
}}
|
||||
>
|
||||
<RotateLeftIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Rotate Right">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const command = new RotatePagesCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
[page.id],
|
||||
90
|
||||
);
|
||||
executeCommand(command);
|
||||
setStatus(`Rotated page ${page.pageNumber} right`);
|
||||
}}
|
||||
>
|
||||
<RotateRightIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Delete Page">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="red"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const command = new DeletePagesCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
[page.id]
|
||||
);
|
||||
executeCommand(command);
|
||||
setStatus(`Deleted page ${page.pageNumber}`);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Split Here">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const command = new ToggleSplitCommand(
|
||||
pdfDocument,
|
||||
setPdfDocument,
|
||||
[page.id]
|
||||
);
|
||||
executeCommand(command);
|
||||
setStatus(`Split marker toggled for page ${page.pageNumber}`);
|
||||
}}
|
||||
>
|
||||
<ContentCutIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Select Page">
|
||||
<Checkbox
|
||||
size="md"
|
||||
checked={selectedPages.includes(page.id)}
|
||||
onChange={() => togglePage(page.id)}
|
||||
styles={{
|
||||
input: { backgroundColor: 'white' }
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<DragIndicatorIcon
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
color: 'rgba(0,0,0,0.3)',
|
||||
fontSize: 16,
|
||||
zIndex: 1
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
<Group justify="space-between" mt="md">
|
||||
<Button
|
||||
color="red"
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setPdfDocument(null);
|
||||
setFile && setFile(null);
|
||||
}}
|
||||
>
|
||||
Close PDF
|
||||
</Button>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
leftSection={<DownloadIcon />}
|
||||
disabled={selectedPages.length === 0 || exportLoading}
|
||||
loading={exportLoading}
|
||||
onClick={() => showExportPreview(true)}
|
||||
>
|
||||
Download Selected
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
disabled={exportLoading}
|
||||
loading={exportLoading}
|
||||
onClick={() => showExportPreview(false)}
|
||||
>
|
||||
Download All
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
{/* Landing zone at the end */}
|
||||
<div
|
||||
data-drop-zone="end"
|
||||
style={{
|
||||
width: '15rem',
|
||||
height: '15rem',
|
||||
border: '2px dashed #9ca3af',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
backgroundColor: dropTarget === 'end' ? '#ecfdf5' : 'transparent',
|
||||
borderColor: dropTarget === 'end' ? '#10b981' : '#9ca3af',
|
||||
transition: 'all 0.2s ease-in-out'
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleEndZoneDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => handleDrop(e, 'end')}
|
||||
>
|
||||
<Text c="dimmed" size="sm" ta="center">
|
||||
Drop here to<br />move to end
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
opened={showExportModal}
|
||||
<Group justify="space-between" mt="md">
|
||||
<Button
|
||||
color="red"
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setPdfDocument(null);
|
||||
setFile && setFile(null);
|
||||
}}
|
||||
>
|
||||
Close PDF
|
||||
</Button>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
leftSection={<DownloadIcon />}
|
||||
disabled={selectedPages.length === 0 || exportLoading}
|
||||
loading={exportLoading}
|
||||
onClick={() => showExportPreview(true)}
|
||||
>
|
||||
Download Selected
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
disabled={exportLoading}
|
||||
loading={exportLoading}
|
||||
onClick={() => showExportPreview(false)}
|
||||
>
|
||||
Download All
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
<Modal
|
||||
opened={showExportModal}
|
||||
onClose={() => setShowExportModal(false)}
|
||||
title="Export Preview"
|
||||
>
|
||||
@@ -536,33 +787,33 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
<Text>Pages to export:</Text>
|
||||
<Text fw={500}>{exportPreview.pageCount}</Text>
|
||||
</Group>
|
||||
|
||||
|
||||
{exportPreview.splitCount > 1 && (
|
||||
<Group justify="space-between">
|
||||
<Text>Split into documents:</Text>
|
||||
<Text fw={500}>{exportPreview.splitCount}</Text>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
|
||||
<Group justify="space-between">
|
||||
<Text>Estimated size:</Text>
|
||||
<Text fw={500}>{exportPreview.estimatedSize}</Text>
|
||||
</Group>
|
||||
|
||||
|
||||
{pdfDocument && pdfDocument.pages.some(p => p.splitBefore) && (
|
||||
<Alert color="blue">
|
||||
This will create multiple PDF files based on split markers.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => setShowExportModal(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
color="green"
|
||||
loading={exportLoading}
|
||||
onClick={() => {
|
||||
@@ -584,20 +835,19 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
||||
onChange={(file) => file && handleFileUpload(file)}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{status && (
|
||||
<Notification
|
||||
color="blue"
|
||||
mt="md"
|
||||
onClose={() => setStatus(null)}
|
||||
style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 1000 }}
|
||||
>
|
||||
{status}
|
||||
</Notification>
|
||||
)}
|
||||
</Container>
|
||||
{status && (
|
||||
<Notification
|
||||
color="blue"
|
||||
mt="md"
|
||||
onClose={() => setStatus(null)}
|
||||
style={{ position: 'fixed', bottom: 20, right: 20, zIndex: 1000 }}
|
||||
>
|
||||
{status}
|
||||
</Notification>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageEditor;
|
||||
export default PageEditor;
|
||||
|
||||
Reference in New Issue
Block a user