mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 15:45:21 +00:00
Page editor updates
This commit is contained in:
parent
7fc850b138
commit
e7995a0256
@ -20,6 +20,8 @@ import ConstructionIcon from "@mui/icons-material/Construction";
|
|||||||
import EventListIcon from "@mui/icons-material/EventList";
|
import EventListIcon from "@mui/icons-material/EventList";
|
||||||
import DeselectIcon from "@mui/icons-material/Deselect";
|
import DeselectIcon from "@mui/icons-material/Deselect";
|
||||||
import SelectAllIcon from "@mui/icons-material/SelectAll";
|
import SelectAllIcon from "@mui/icons-material/SelectAll";
|
||||||
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
|
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
|
||||||
import { usePDFProcessor } from "../hooks/usePDFProcessor";
|
import { usePDFProcessor } from "../hooks/usePDFProcessor";
|
||||||
import { PDFDocument, PDFPage } from "../types/pageEditor";
|
import { PDFDocument, PDFPage } from "../types/pageEditor";
|
||||||
import { fileStorage } from "../services/fileStorage";
|
import { fileStorage } from "../services/fileStorage";
|
||||||
@ -62,6 +64,7 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
const [exportLoading, setExportLoading] = useState(false);
|
const [exportLoading, setExportLoading] = useState(false);
|
||||||
const [showExportModal, setShowExportModal] = useState(false);
|
const [showExportModal, setShowExportModal] = useState(false);
|
||||||
const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null);
|
const [exportPreview, setExportPreview] = useState<{pageCount: number; splitCount: number; estimatedSize: string} | null>(null);
|
||||||
|
const [movingPage, setMovingPage] = useState<string | null>(null);
|
||||||
const fileInputRef = useRef<() => void>(null);
|
const fileInputRef = useRef<() => void>(null);
|
||||||
|
|
||||||
// Undo/Redo system
|
// Undo/Redo system
|
||||||
@ -163,13 +166,13 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!draggedPage) return;
|
if (!draggedPage) return;
|
||||||
|
|
||||||
// Get the element under the mouse cursor
|
// Get the element under the mouse cursor
|
||||||
const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY);
|
const elementUnderCursor = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
if (!elementUnderCursor) return;
|
if (!elementUnderCursor) return;
|
||||||
|
|
||||||
// Find the closest page container
|
// Find the closest page container
|
||||||
const pageContainer = elementUnderCursor.closest('[data-page-id]');
|
const pageContainer = elementUnderCursor.closest('[data-page-id]');
|
||||||
if (pageContainer) {
|
if (pageContainer) {
|
||||||
@ -179,14 +182,14 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if over the end zone
|
// Check if over the end zone
|
||||||
const endZone = elementUnderCursor.closest('[data-drop-zone="end"]');
|
const endZone = elementUnderCursor.closest('[data-drop-zone="end"]');
|
||||||
if (endZone) {
|
if (endZone) {
|
||||||
setDropTarget('end');
|
setDropTarget('end');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not over any valid drop target, clear it
|
// If not over any valid drop target, clear it
|
||||||
setDropTarget(null);
|
setDropTarget(null);
|
||||||
}, [draggedPage]);
|
}, [draggedPage]);
|
||||||
@ -345,33 +348,28 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
|
<Box pos="relative" h="100vh" style={{ overflow: 'auto' }}>
|
||||||
<LoadingOverlay visible={loading || pdfLoading} />
|
<LoadingOverlay visible={loading || pdfLoading} />
|
||||||
|
|
||||||
<Box p="xl">
|
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
{error && (
|
|
||||||
<Alert color="red" mb="md" onClose={() => setError(null)}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => files[0] && handleFileUpload(files[0])}
|
onDrop={(files) => files[0] && handleFileUpload(files[0])}
|
||||||
accept={["application/pdf"]}
|
accept={["application/pdf"]}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
h="60vh"
|
h="60vh"
|
||||||
style={{ minHeight: 400 }}
|
style={{ minHeight: 400 }}
|
||||||
>
|
>
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<Stack align="center" gap="md">
|
<Stack align="center" gap="md">
|
||||||
<UploadFileIcon style={{ fontSize: 64 }} />
|
<UploadFileIcon style={{ fontSize: 64 }} />
|
||||||
<Text size="xl" fw={500}>
|
<Text size="xl" fw={500}>
|
||||||
Drop a PDF file here or click to upload
|
Drop a PDF file here or click to upload
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="md" c="dimmed">
|
<Text size="md" c="dimmed">
|
||||||
Supports PDF files only
|
Supports PDF files only
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
</Box>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -392,6 +390,14 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
.page-container:hover {
|
.page-container:hover {
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
.page-move-animation {
|
||||||
|
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
}
|
||||||
|
.page-moving {
|
||||||
|
z-index: 10;
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@ -489,30 +495,15 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
{page.splitBefore && index > 0 && (
|
{page.splitBefore && index > 0 && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '4px',
|
width: '2px',
|
||||||
height: '15rem',
|
height: '20rem',
|
||||||
border: '2px dashed #3b82f6',
|
borderLeft: '2px dashed #3b82f6',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
borderRadius: '2px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginLeft: '-0.75rem',
|
marginLeft: '-0.75rem',
|
||||||
marginRight: '-0.75rem',
|
marginRight: '-0.75rem',
|
||||||
position: 'relative',
|
|
||||||
flexShrink: 0
|
flexShrink: 0
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<ContentCutIcon
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
color: '#3b82f6',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderRadius: '50%',
|
|
||||||
padding: '3px'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
data-page-id={page.id}
|
data-page-id={page.id}
|
||||||
@ -520,23 +511,25 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
!rounded-lg
|
!rounded-lg
|
||||||
cursor-grab
|
cursor-grab
|
||||||
select-none
|
select-none
|
||||||
w-[15rem]
|
w-[20rem]
|
||||||
h-[15rem]
|
h-[20rem]
|
||||||
flex items-center justify-center
|
flex items-center justify-center
|
||||||
flex-shrink-0
|
flex-shrink-0
|
||||||
shadow-sm
|
shadow-sm
|
||||||
hover:shadow-md
|
hover:shadow-md
|
||||||
transition-all
|
transition-all
|
||||||
relative
|
relative
|
||||||
|
page-move-animation
|
||||||
${selectedPages.includes(page.id)
|
${selectedPages.includes(page.id)
|
||||||
? 'ring-2 ring-blue-500 bg-blue-50'
|
? 'ring-2 ring-blue-500 bg-blue-50'
|
||||||
: 'bg-white hover:bg-gray-50'}
|
: 'bg-white hover:bg-gray-50'}
|
||||||
${draggedPage === page.id ? 'opacity-50 scale-95' : ''}
|
${draggedPage === page.id ? 'opacity-50 scale-95' : ''}
|
||||||
|
${movingPage === page.id ? 'page-moving' : ''}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
transform: (() => {
|
transform: (() => {
|
||||||
if (!draggedPage || page.id === draggedPage) return 'translateX(0)';
|
if (!draggedPage || page.id === draggedPage) return 'translateX(0)';
|
||||||
|
|
||||||
if (dropTarget === page.id) {
|
if (dropTarget === page.id) {
|
||||||
return 'translateX(20px)'; // Move slightly right to indicate drop position
|
return 'translateX(20px)'; // Move slightly right to indicate drop position
|
||||||
}
|
}
|
||||||
@ -606,6 +599,62 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<Tooltip label="Move Left">
|
||||||
|
<ActionIcon
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
c="white"
|
||||||
|
disabled={index === 0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (index > 0 && !movingPage) {
|
||||||
|
setMovingPage(page.id);
|
||||||
|
setTimeout(() => {
|
||||||
|
const command = new ReorderPageCommand(
|
||||||
|
pdfDocument,
|
||||||
|
setPdfDocument,
|
||||||
|
page.id,
|
||||||
|
index - 1
|
||||||
|
);
|
||||||
|
executeCommand(command);
|
||||||
|
setTimeout(() => setMovingPage(null), 100);
|
||||||
|
setStatus(`Moved page ${page.pageNumber} left`);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowBackIcon style={{ fontSize: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="Move Right">
|
||||||
|
<ActionIcon
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
c="white"
|
||||||
|
disabled={index === pdfDocument.pages.length - 1}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (index < pdfDocument.pages.length - 1 && !movingPage) {
|
||||||
|
setMovingPage(page.id);
|
||||||
|
setTimeout(() => {
|
||||||
|
const command = new ReorderPageCommand(
|
||||||
|
pdfDocument,
|
||||||
|
setPdfDocument,
|
||||||
|
page.id,
|
||||||
|
index + 1
|
||||||
|
);
|
||||||
|
executeCommand(command);
|
||||||
|
setTimeout(() => setMovingPage(null), 100);
|
||||||
|
setStatus(`Moved page ${page.pageNumber} right`);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowForwardIcon style={{ fontSize: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label="Rotate Left">
|
<Tooltip label="Rotate Left">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
size="md"
|
size="md"
|
||||||
@ -668,25 +717,27 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip label="Split Here">
|
{index > 0 && (
|
||||||
<ActionIcon
|
<Tooltip label="Split Here">
|
||||||
size="md"
|
<ActionIcon
|
||||||
variant="subtle"
|
size="md"
|
||||||
c="white"
|
variant="subtle"
|
||||||
onClick={(e) => {
|
c="white"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
const command = new ToggleSplitCommand(
|
e.stopPropagation();
|
||||||
pdfDocument,
|
const command = new ToggleSplitCommand(
|
||||||
setPdfDocument,
|
pdfDocument,
|
||||||
[page.id]
|
setPdfDocument,
|
||||||
);
|
[page.id]
|
||||||
executeCommand(command);
|
);
|
||||||
setStatus(`Split marker toggled for page ${page.pageNumber}`);
|
executeCommand(command);
|
||||||
}}
|
setStatus(`Split marker toggled for page ${page.pageNumber}`);
|
||||||
>
|
}}
|
||||||
<ContentCutIcon style={{ fontSize: 20 }} />
|
>
|
||||||
</ActionIcon>
|
<ContentCutIcon style={{ fontSize: 20 }} />
|
||||||
</Tooltip>
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip label="Select Page">
|
<Tooltip label="Select Page">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@ -714,31 +765,24 @@ const PageEditor: React.FC<PageEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Landing zone at the end */}
|
{/* Landing zone at the end */}
|
||||||
<div
|
<div className="w-[20rem] h-[20rem] flex items-center justify-center flex-shrink-0">
|
||||||
data-drop-zone="end"
|
<div
|
||||||
style={{
|
data-drop-zone="end"
|
||||||
width: '15rem',
|
className={`cursor-pointer select-none w-[15rem] h-[15rem] flex items-center justify-center flex-shrink-0 shadow-sm hover:shadow-md transition-all relative ${dropTarget === 'end' ? 'ring-2 ring-green-500 bg-green-50' : 'bg-white hover:bg-blue-50 border-2 border-dashed border-gray-300 hover:border-blue-400'}`}
|
||||||
height: '15rem',
|
style={{
|
||||||
border: '2px dashed #9ca3af',
|
borderRadius: '12px'
|
||||||
borderRadius: '8px',
|
}}
|
||||||
display: 'flex',
|
onDragOver={handleDragOver}
|
||||||
alignItems: 'center',
|
onDragEnter={handleEndZoneDragEnter}
|
||||||
justifyContent: 'center',
|
onDragLeave={handleDragLeave}
|
||||||
flexShrink: 0,
|
onDrop={(e) => handleDrop(e, 'end')}
|
||||||
backgroundColor: dropTarget === 'end' ? '#ecfdf5' : 'transparent',
|
>
|
||||||
borderColor: dropTarget === 'end' ? '#10b981' : '#9ca3af',
|
<Text c="dimmed" size="sm" ta="center" fw={500}>
|
||||||
transition: 'all 0.2s ease-in-out'
|
Drop here to<br />move to end
|
||||||
}}
|
</Text>
|
||||||
onDragOver={handleDragOver}
|
</div>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user