mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 07:35:22 +00:00
Update Page editor styling
This commit is contained in:
parent
ac3da9b7c2
commit
7fc850b138
@ -246,7 +246,7 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
|
||||
## 6. Testing
|
||||
## 7. Testing
|
||||
|
||||
### Comprehensive Testing Script
|
||||
|
||||
@ -311,7 +311,7 @@ Important notes:
|
||||
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
||||
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
||||
|
||||
## 7. Contributing
|
||||
## 8. Contributing
|
||||
|
||||
1. Fork the repository on GitHub.
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
@ -336,11 +336,11 @@ When you raise a PR:
|
||||
|
||||
Address any issues that arise from these checks before finalizing your pull request.
|
||||
|
||||
## 8. API Documentation
|
||||
## 9. API Documentation
|
||||
|
||||
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
|
||||
|
||||
## 9. Customization
|
||||
## 10. Customization
|
||||
|
||||
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
|
||||
|
||||
@ -359,7 +359,7 @@ docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
|
||||
|
||||
Refer to the main README for a full list of customization options.
|
||||
|
||||
## 10. Language Translations
|
||||
## 11. Language Translations
|
||||
|
||||
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
|
||||
|
||||
|
@ -43,7 +43,7 @@ export class RotatePagesCommand extends PageCommand {
|
||||
execute(): void {
|
||||
const updatedPages = this.pdfDocument.pages.map(page => {
|
||||
if (this.pageIds.includes(page.id)) {
|
||||
return { ...page, rotation: (page.rotation + this.rotation) % 360 };
|
||||
return { ...page, rotation: page.rotation + this.rotation };
|
||||
}
|
||||
return page;
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import '@mantine/core/styles.css';
|
||||
import './index.css'; // Import Tailwind CSS
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@mantine/core';
|
||||
|
Loading…
x
Reference in New Issue
Block a user