mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-29 16:45:37 +00:00
- Viewer overhaul
-Dark mode toggle -URL params improvements -app.js set up fix - UI clean up
This commit is contained in:
parent
41c82b15da
commit
d216811317
@ -1,31 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
|
||||
import './index.css';
|
||||
import HomePage from './pages/HomePage';
|
||||
import SplitPdfPanel from './tools/Split';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/split" element={<SplitPdfPanel />} />
|
||||
</Routes>
|
||||
);
|
||||
export default function App({ colorScheme, toggleColorScheme }) {
|
||||
return <HomePage colorScheme={colorScheme} toggleColorScheme={toggleColorScheme} />;
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<ColorSchemeScript />
|
||||
<MantineProvider withGlobalStyles withNormalizeCSS>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</MantineProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
reportWebVitals();
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Group, Text, Stack, Image, Badge, Button, Box, Flex } from "@mantine/core";
|
||||
import { Card, Group, Text, Stack, Image, Badge, Button, Box, Flex, ThemeIcon } from "@mantine/core";
|
||||
import { Dropzone, MIME_TYPES } from "@mantine/dropzone";
|
||||
import { GlobalWorkerOptions, getDocument } from "pdfjs-dist";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
|
||||
GlobalWorkerOptions.workerSrc =
|
||||
(import.meta as any).env?.PUBLIC_URL
|
||||
@ -93,7 +94,15 @@ function FileCard({ file, onRemove, onDoubleClick }: FileCardProps) {
|
||||
{thumb ? (
|
||||
<Image src={thumb} alt="PDF thumbnail" height={110} width={80} fit="contain" radius="sm" />
|
||||
) : (
|
||||
<Image src="/images/pdf-placeholder.svg" alt="PDF" height={60} width={60} fit="contain" radius="sm" />
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
color="red"
|
||||
size={60}
|
||||
radius="sm"
|
||||
style={{ display: "flex", alignItems: "center", justifyContent: "center" }}
|
||||
>
|
||||
<PictureAsPdfIcon style={{ fontSize: 40 }} />
|
||||
</ThemeIcon>
|
||||
)}
|
||||
</Box>
|
||||
<Text fw={500} size="sm" lineClamp={1} ta="center">
|
||||
@ -145,21 +154,22 @@ const FileManager: React.FC<FileManagerProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", margin: "0 auto" }}>
|
||||
<div style={{ width: "100%", margin: "0 auto", justifyContent: "center", display: "flex", flexDirection: "column", alignItems: "center", padding: "20px" }}>
|
||||
<Dropzone
|
||||
onDrop={handleDrop}
|
||||
accept={[MIME_TYPES.pdf]}
|
||||
multiple={allowMultiple}
|
||||
maxSize={20 * 1024 * 1024}
|
||||
style={{
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
border: "2px dashed rgb(202, 202, 202)",
|
||||
background: "#f8fafc",
|
||||
borderRadius: 8,
|
||||
minHeight: 120,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width:"90%"
|
||||
}}
|
||||
>
|
||||
<Group justify="center" gap="xl" style={{ pointerEvents: "none" }}>
|
||||
|
@ -27,7 +27,6 @@ const ToolPicker: React.FC<ToolPickerProps> = ({ selectedToolKey, onSelect, tool
|
||||
<Box
|
||||
style={{
|
||||
width: 220,
|
||||
background: "#f8f9fa",
|
||||
borderRight: "1px solid #e9ecef",
|
||||
minHeight: "100vh",
|
||||
padding: 16,
|
||||
|
@ -1,18 +1,121 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group } from "@mantine/core";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group, NumberInput, useMantineTheme } from "@mantine/core";
|
||||
import { getDocument, GlobalWorkerOptions } from "pdfjs-dist";
|
||||
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import FirstPageIcon from "@mui/icons-material/FirstPage";
|
||||
import LastPageIcon from "@mui/icons-material/LastPage";
|
||||
import ViewSidebarIcon from "@mui/icons-material/ViewSidebar";
|
||||
import ViewWeekIcon from "@mui/icons-material/ViewWeek"; // for dual page (book)
|
||||
import DescriptionIcon from "@mui/icons-material/Description"; // for single page
|
||||
import { useLocalStorage } from "@mantine/hooks";
|
||||
|
||||
GlobalWorkerOptions.workerSrc = `${process.env.PUBLIC_URL}/pdf.worker.js`;
|
||||
|
||||
export interface ViewerProps {
|
||||
pdfFile: { file: File; url: string } | null;
|
||||
setPdfFile: (file: { file: File; url: string } | null) => void;
|
||||
sidebarsVisible: boolean;
|
||||
setSidebarsVisible: (v: boolean) => void;
|
||||
}
|
||||
|
||||
const Viewer: React.FC<ViewerProps> = ({ pdfFile, setPdfFile }) => {
|
||||
const Viewer: React.FC<ViewerProps> = ({
|
||||
pdfFile,
|
||||
setPdfFile,
|
||||
sidebarsVisible,
|
||||
setSidebarsVisible,
|
||||
}) => {
|
||||
const theme = useMantineTheme();
|
||||
const [numPages, setNumPages] = useState<number>(0);
|
||||
const [pageImages, setPageImages] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [currentPage, setCurrentPage] = useState<number | null>(null);
|
||||
const [dualPage, setDualPage] = useState(false);
|
||||
const [zoom, setZoom] = useState(1); // 1 = 100%
|
||||
const pageRefs = useRef<(HTMLImageElement | null)[]>([]);
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const userInitiatedRef = useRef(false);
|
||||
const suppressScrollRef = useRef(false);
|
||||
|
||||
|
||||
// Listen for hash changes and update currentPage
|
||||
useEffect(() => {
|
||||
function handleHashChange() {
|
||||
if (window.location.hash.startsWith("#page=")) {
|
||||
const page = parseInt(window.location.hash.replace("#page=", ""), 10);
|
||||
if (!isNaN(page) && page >= 1 && page <= numPages) {
|
||||
setCurrentPage(page);
|
||||
}
|
||||
}
|
||||
userInitiatedRef.current = false;
|
||||
}
|
||||
window.addEventListener("hashchange", handleHashChange);
|
||||
handleHashChange(); // Run on mount
|
||||
return () => window.removeEventListener("hashchange", handleHashChange);
|
||||
}, [numPages]);
|
||||
|
||||
// Scroll to the current page when it changes
|
||||
useEffect(() => {
|
||||
if (currentPage && pageRefs.current[currentPage - 1]) {
|
||||
suppressScrollRef.current = true;
|
||||
const el = pageRefs.current[currentPage - 1];
|
||||
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
|
||||
// Try to use scrollend if supported
|
||||
const viewport = scrollAreaRef.current;
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
let scrollEndHandler: (() => void) | null = null;
|
||||
|
||||
if (viewport && "onscrollend" in viewport) {
|
||||
scrollEndHandler = () => {
|
||||
suppressScrollRef.current = false;
|
||||
viewport.removeEventListener("scrollend", scrollEndHandler!);
|
||||
};
|
||||
viewport.addEventListener("scrollend", scrollEndHandler);
|
||||
} else {
|
||||
// Fallback for non-Chromium browsers
|
||||
timeout = setTimeout(() => {
|
||||
suppressScrollRef.current = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (viewport && scrollEndHandler) {
|
||||
viewport.removeEventListener("scrollend", scrollEndHandler);
|
||||
}
|
||||
if (timeout) clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [currentPage, pageImages]);
|
||||
|
||||
// Detect visible page on scroll and update hash
|
||||
const handleScroll = () => {
|
||||
if (suppressScrollRef.current) return;
|
||||
const scrollArea = scrollAreaRef.current;
|
||||
if (!scrollArea || !pageRefs.current.length) return;
|
||||
|
||||
const areaRect = scrollArea.getBoundingClientRect();
|
||||
let closestIdx = 0;
|
||||
let minDist = Infinity;
|
||||
|
||||
pageRefs.current.forEach((img, idx) => {
|
||||
if (img) {
|
||||
const imgRect = img.getBoundingClientRect();
|
||||
const dist = Math.abs(imgRect.top - areaRect.top);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closestIdx = idx;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (currentPage !== closestIdx + 1) {
|
||||
setCurrentPage(closestIdx + 1);
|
||||
if (window.location.hash !== `#page=${closestIdx + 1}`) {
|
||||
window.location.hash = `#page=${closestIdx + 1}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@ -49,12 +152,31 @@ const Viewer: React.FC<ViewerProps> = ({ pdfFile, setPdfFile }) => {
|
||||
return () => { cancelled = true; };
|
||||
}, [pdfFile]);
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = scrollAreaRef.current;
|
||||
if (!viewport) return;
|
||||
const handler = () => {
|
||||
handleScroll();
|
||||
};
|
||||
viewport.addEventListener("scroll", handler);
|
||||
return () => viewport.removeEventListener("scroll", handler);
|
||||
}, [pageImages]);
|
||||
|
||||
return (
|
||||
<Paper shadow="xs" radius="md" p="md" style={{ height: "100%", minHeight: 400, display: "flex", flexDirection: "column" }}>
|
||||
<Paper
|
||||
shadow="xs"
|
||||
radius="md"
|
||||
style={{
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{!pdfFile ? (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Stack align="center">
|
||||
<Text color="dimmed">No PDF loaded. Click to upload a PDF.</Text>
|
||||
<Text c="dimmed">No PDF loaded. Click to upload a PDF.</Text>
|
||||
<Button
|
||||
component="label"
|
||||
variant="outline"
|
||||
@ -81,39 +203,222 @@ const Viewer: React.FC<ViewerProps> = ({ pdfFile, setPdfFile }) => {
|
||||
<Loader size="lg" />
|
||||
</Center>
|
||||
) : (
|
||||
<ScrollArea style={{ flex: 1, height: "100%" }}>
|
||||
<Stack gap="xl" align="center">
|
||||
<ScrollArea
|
||||
style={{ flex: 1, height: "100%", position: "relative"}}
|
||||
viewportRef={scrollAreaRef}
|
||||
>
|
||||
<Stack gap="xl" align="center" >
|
||||
{pageImages.length === 0 && (
|
||||
<Text color="dimmed">No pages to display.</Text>
|
||||
)}
|
||||
{pageImages.map((img, idx) => (
|
||||
<img
|
||||
key={idx}
|
||||
src={img}
|
||||
alt={`Page ${idx + 1}`}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 700,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||||
borderRadius: 8,
|
||||
background: "#fff"
|
||||
{dualPage
|
||||
? Array.from({ length: Math.ceil(pageImages.length / 2) }).map((_, i) => (
|
||||
<Group key={i} gap="md" align="flex-start" style={{ width: "100%", justifyContent: "center" }}>
|
||||
<img
|
||||
ref={el => { pageRefs.current[i * 2] = el; }}
|
||||
src={pageImages[i * 2]}
|
||||
alt={`Page ${i * 2 + 1}`}
|
||||
style={{
|
||||
width: `${100 * zoom}%`,
|
||||
maxWidth: 700 * zoom,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||||
borderRadius: 8,
|
||||
marginTop: i === 0 ? theme.spacing.xl : 0, // <-- add gap to first row
|
||||
}}
|
||||
/>
|
||||
{pageImages[i * 2 + 1] && (
|
||||
<img
|
||||
ref={el => { pageRefs.current[i * 2 + 1] = el; }}
|
||||
src={pageImages[i * 2 + 1]}
|
||||
alt={`Page ${i * 2 + 2}`}
|
||||
style={{
|
||||
width: `${100 * zoom}%`,
|
||||
maxWidth: 700 * zoom,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||||
borderRadius: 8,
|
||||
marginTop: i === 0 ? theme.spacing.xl : 0, // <-- add gap to first row
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
))
|
||||
: pageImages.map((img, idx) => (
|
||||
<img
|
||||
key={idx}
|
||||
ref={el => { pageRefs.current[idx] = el; }}
|
||||
src={img}
|
||||
alt={`Page ${idx + 1}`}
|
||||
style={{
|
||||
width: `${100 * zoom}%`,
|
||||
maxWidth: 700 * zoom,
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
|
||||
borderRadius: 8,
|
||||
marginTop: idx === 0 ? theme.spacing.xl : 0, // <-- add gap to first page
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
{/* Navigation bar overlays the scroll area */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 50,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
pointerEvents: "none",
|
||||
background: "transparent",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
radius="xl xl 0 0"
|
||||
shadow="sm"
|
||||
p={12}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
boxShadow: "0 -2px 8px rgba(0,0,0,0.04)",
|
||||
pointerEvents: "auto",
|
||||
minWidth: 420,
|
||||
maxWidth: 700,
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="md"
|
||||
px={8}
|
||||
radius="xl"
|
||||
onClick={() => {
|
||||
window.location.hash = `#page=1`;
|
||||
}}
|
||||
disabled={currentPage === 1}
|
||||
style={{ minWidth: 36 }}
|
||||
>
|
||||
<FirstPageIcon fontSize="small" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="md"
|
||||
px={8}
|
||||
radius="xl"
|
||||
onClick={() => {
|
||||
window.location.hash = `#page=${Math.max(1, (currentPage || 1) - 1)}`;
|
||||
}}
|
||||
disabled={currentPage === 1}
|
||||
style={{ minWidth: 36 }}
|
||||
>
|
||||
<ArrowBackIosNewIcon fontSize="small" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={currentPage || 1}
|
||||
onChange={value => {
|
||||
const page = Number(value);
|
||||
if (!isNaN(page) && page >= 1 && page <= numPages) {
|
||||
window.location.hash = `#page=${page}`;
|
||||
}
|
||||
}}
|
||||
min={1}
|
||||
max={numPages}
|
||||
hideControls
|
||||
styles={{
|
||||
input: { width: 48, textAlign: "center", fontWeight: 500, fontSize: 16},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
<span style={{ fontWeight: 500, fontSize: 16 }}>
|
||||
/ {numPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="md"
|
||||
px={8}
|
||||
radius="xl"
|
||||
onClick={() => {
|
||||
window.location.hash = `#page=${Math.min(numPages, (currentPage || 1) + 1)}`;
|
||||
}}
|
||||
disabled={currentPage === numPages}
|
||||
style={{ minWidth: 36 }}
|
||||
>
|
||||
<ArrowForwardIosIcon fontSize="small" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="md"
|
||||
px={8}
|
||||
radius="xl"
|
||||
onClick={() => {
|
||||
window.location.hash = `#page=${numPages}`;
|
||||
}}
|
||||
disabled={currentPage === numPages}
|
||||
style={{ minWidth: 36 }}
|
||||
>
|
||||
<LastPageIcon fontSize="small" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={dualPage ? "filled" : "light"}
|
||||
color="blue"
|
||||
size="md"
|
||||
radius="xl"
|
||||
onClick={() => setDualPage(v => !v)}
|
||||
style={{ minWidth: 36 }}
|
||||
title={dualPage ? "Single Page View" : "Dual Page View"}
|
||||
>
|
||||
{dualPage ? <DescriptionIcon fontSize="small" /> : <ViewWeekIcon fontSize="small" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="md"
|
||||
radius="xl"
|
||||
onClick={() => setSidebarsVisible(!sidebarsVisible)}
|
||||
style={{ minWidth: 36 }}
|
||||
title={sidebarsVisible ? "Hide Sidebars" : "Show Sidebars"}
|
||||
>
|
||||
<ViewSidebarIcon
|
||||
fontSize="small"
|
||||
style={{
|
||||
transform: sidebarsVisible ? "none" : "scaleX(-1)",
|
||||
transition: "transform 0.2s"
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Group gap={4} align="center" style={{ marginLeft: 16 }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="md"
|
||||
radius="xl"
|
||||
onClick={() => setZoom(z => Math.max(0.1, z - 0.1))}
|
||||
style={{ minWidth: 32, padding: 0 }}
|
||||
title="Zoom out"
|
||||
>−</Button>
|
||||
<span style={{ minWidth: 40, textAlign: "center" }}>{Math.round(zoom * 100)}%</span>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="blue"
|
||||
size="md"
|
||||
radius="xl"
|
||||
onClick={() => setZoom(z => Math.min(5, z + 0.1))}
|
||||
style={{ minWidth: 32, padding: 0 }}
|
||||
title="Zoom in"
|
||||
>+</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
{pdfFile && (
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={() => setPdfFile(null)}
|
||||
>
|
||||
Close PDF
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root')); // Finds the root DOM element
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
|
@ -6,7 +6,7 @@ import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||
import EditNoteIcon from "@mui/icons-material/EditNote";
|
||||
import { Group, SegmentedControl, Paper, Center, Box } from "@mantine/core";
|
||||
import { Group, SegmentedControl, Paper, Center, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
||||
|
||||
import ToolPicker from "../components/ToolPicker";
|
||||
import FileManager from "../components/FileManager";
|
||||
@ -15,6 +15,8 @@ import CompressPdfPanel from "../tools/Compress";
|
||||
import MergePdfPanel from "../tools/Merge";
|
||||
import PageEditor from "../components/PageEditor";
|
||||
import Viewer from "../components/Viewer";
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
|
||||
type ToolRegistryEntry = {
|
||||
icon: React.ReactNode;
|
||||
@ -60,8 +62,98 @@ const VIEW_OPTIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
// Utility to extract params for a tool from searchParams
|
||||
function getToolParams(toolKey: string, searchParams: URLSearchParams) {
|
||||
switch (toolKey) {
|
||||
case "split":
|
||||
return {
|
||||
mode: searchParams.get("splitMode") || "byPages",
|
||||
pages: searchParams.get("pages") || "",
|
||||
hDiv: searchParams.get("hDiv") || "0",
|
||||
vDiv: searchParams.get("vDiv") || "1",
|
||||
merge: searchParams.get("merge") === "true",
|
||||
splitType: searchParams.get("splitType") || "size",
|
||||
splitValue: searchParams.get("splitValue") || "",
|
||||
bookmarkLevel: searchParams.get("bookmarkLevel") || "0",
|
||||
includeMetadata: searchParams.get("includeMetadata") === "true",
|
||||
allowDuplicates: searchParams.get("allowDuplicates") === "true",
|
||||
};
|
||||
case "compress":
|
||||
return {
|
||||
level: searchParams.get("compressLevel") || "medium",
|
||||
keepQuality: searchParams.get("keepQuality") === "true",
|
||||
};
|
||||
case "merge":
|
||||
return {
|
||||
order: searchParams.get("mergeOrder") || "default",
|
||||
removeDuplicates: searchParams.get("removeDuplicates") === "true",
|
||||
};
|
||||
// Add more tools here as needed
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Utility to update params for a tool
|
||||
function updateToolParams(toolKey: string, searchParams: URLSearchParams, setSearchParams: any, newParams: any) {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
// Clear tool-specific params
|
||||
if (toolKey === "split") {
|
||||
[
|
||||
"splitMode", "pages", "hDiv", "vDiv", "merge",
|
||||
"splitType", "splitValue", "bookmarkLevel", "includeMetadata", "allowDuplicates"
|
||||
].forEach((k) => params.delete(k));
|
||||
// Set new split params
|
||||
const merged = { ...getToolParams("split", searchParams), ...newParams };
|
||||
params.set("splitMode", merged.mode);
|
||||
if (merged.mode === "byPages") params.set("pages", merged.pages);
|
||||
else if (merged.mode === "bySections") {
|
||||
params.set("hDiv", merged.hDiv);
|
||||
params.set("vDiv", merged.vDiv);
|
||||
params.set("merge", String(merged.merge));
|
||||
} else if (merged.mode === "bySizeOrCount") {
|
||||
params.set("splitType", merged.splitType);
|
||||
params.set("splitValue", merged.splitValue);
|
||||
} else if (merged.mode === "byChapters") {
|
||||
params.set("bookmarkLevel", merged.bookmarkLevel);
|
||||
params.set("includeMetadata", String(merged.includeMetadata));
|
||||
params.set("allowDuplicates", String(merged.allowDuplicates));
|
||||
}
|
||||
} else if (toolKey === "compress") {
|
||||
["compressLevel", "keepQuality"].forEach((k) => params.delete(k));
|
||||
const merged = { ...getToolParams("compress", searchParams), ...newParams };
|
||||
params.set("compressLevel", merged.level);
|
||||
params.set("keepQuality", String(merged.keepQuality));
|
||||
} else if (toolKey === "merge") {
|
||||
["mergeOrder", "removeDuplicates"].forEach((k) => params.delete(k));
|
||||
const merged = { ...getToolParams("merge", searchParams), ...newParams };
|
||||
params.set("mergeOrder", merged.order);
|
||||
params.set("removeDuplicates", String(merged.removeDuplicates));
|
||||
}
|
||||
// Add more tools as needed
|
||||
|
||||
setSearchParams(params, { replace: true });
|
||||
}
|
||||
|
||||
// List of all tool-specific params
|
||||
const TOOL_PARAMS = {
|
||||
split: [
|
||||
"splitMode", "pages", "hDiv", "vDiv", "merge",
|
||||
"splitType", "splitValue", "bookmarkLevel", "includeMetadata", "allowDuplicates"
|
||||
],
|
||||
compress: [
|
||||
"compressLevel", "keepQuality"
|
||||
],
|
||||
merge: [
|
||||
"mergeOrder", "removeDuplicates"
|
||||
]
|
||||
// Add more tools as needed
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
// Core app state
|
||||
const [selectedToolKey, setSelectedToolKey] = useState<string>(searchParams.get("tool") || "split");
|
||||
@ -69,28 +161,37 @@ export default function HomePage() {
|
||||
const [pdfFile, setPdfFile] = useState<any>(null);
|
||||
const [files, setFiles] = useState<any[]>([]);
|
||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||
|
||||
// Tool-specific parameters
|
||||
const [splitParams, setSplitParams] = useState({
|
||||
mode: searchParams.get("splitMode") || "byPages",
|
||||
pages: searchParams.get("pages") || "",
|
||||
hDiv: searchParams.get("hDiv") || "0",
|
||||
vDiv: searchParams.get("vDiv") || "1",
|
||||
merge: searchParams.get("merge") === "true",
|
||||
splitType: searchParams.get("splitType") || "size",
|
||||
splitValue: searchParams.get("splitValue") || "",
|
||||
bookmarkLevel: searchParams.get("bookmarkLevel") || "0",
|
||||
includeMetadata: searchParams.get("includeMetadata") === "true",
|
||||
allowDuplicates: searchParams.get("allowDuplicates") === "true",
|
||||
});
|
||||
const toolParams = getToolParams(selectedToolKey, searchParams);
|
||||
|
||||
const updateParams = (newParams: any) =>
|
||||
updateToolParams(selectedToolKey, searchParams, setSearchParams, newParams);
|
||||
|
||||
// Update URL when core state changes
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("tool", selectedToolKey);
|
||||
params.set("view", currentView);
|
||||
setSearchParams(params, { replace: true });
|
||||
}, [selectedToolKey, currentView, setSearchParams]);
|
||||
|
||||
// Remove all tool-specific params except for the current tool
|
||||
Object.entries(TOOL_PARAMS).forEach(([tool, keys]) => {
|
||||
if (tool !== selectedToolKey) {
|
||||
keys.forEach((k) => params.delete(k));
|
||||
}
|
||||
});
|
||||
|
||||
// Collect all params except 'view'
|
||||
const entries = Array.from(params.entries()).filter(([key]) => key !== "view");
|
||||
|
||||
// Rebuild params with 'view' first
|
||||
const newParams = new URLSearchParams();
|
||||
newParams.set("view", currentView);
|
||||
newParams.set("tool", selectedToolKey);
|
||||
entries.forEach(([key, value]) => {
|
||||
if (key !== "tool") newParams.set(key, value);
|
||||
});
|
||||
|
||||
setSearchParams(newParams, { replace: true });
|
||||
}, [selectedToolKey, currentView, setSearchParams, searchParams]);
|
||||
|
||||
// Handle tool selection
|
||||
const handleToolSelect = useCallback(
|
||||
@ -101,51 +202,6 @@ export default function HomePage() {
|
||||
[toolRegistry]
|
||||
);
|
||||
|
||||
// Handle split parameter updates
|
||||
const updateSplitParams = useCallback((newParams: Partial<typeof splitParams>) => {
|
||||
setSplitParams(prev => {
|
||||
const updated = { ...prev, ...newParams };
|
||||
|
||||
// Update URL with split params
|
||||
const params = new URLSearchParams(searchParams);
|
||||
|
||||
// Clear old parameters when mode changes
|
||||
if (newParams.mode && newParams.mode !== prev.mode) {
|
||||
params.delete("pages");
|
||||
params.delete("hDiv");
|
||||
params.delete("vDiv");
|
||||
params.delete("merge");
|
||||
params.delete("splitType");
|
||||
params.delete("splitValue");
|
||||
params.delete("bookmarkLevel");
|
||||
params.delete("includeMetadata");
|
||||
params.delete("allowDuplicates");
|
||||
}
|
||||
|
||||
// Set the mode
|
||||
params.set("splitMode", updated.mode);
|
||||
|
||||
// Set mode-specific parameters
|
||||
if (updated.mode === "byPages" && updated.pages) {
|
||||
params.set("pages", updated.pages);
|
||||
} else if (updated.mode === "bySections") {
|
||||
params.set("hDiv", updated.hDiv);
|
||||
params.set("vDiv", updated.vDiv);
|
||||
params.set("merge", String(updated.merge));
|
||||
} else if (updated.mode === "bySizeOrCount") {
|
||||
params.set("splitType", updated.splitType);
|
||||
if (updated.splitValue) params.set("splitValue", updated.splitValue);
|
||||
} else if (updated.mode === "byChapters") {
|
||||
params.set("bookmarkLevel", updated.bookmarkLevel);
|
||||
params.set("includeMetadata", String(updated.includeMetadata));
|
||||
params.set("allowDuplicates", String(updated.allowDuplicates));
|
||||
}
|
||||
|
||||
setSearchParams(params, { replace: true });
|
||||
return updated;
|
||||
});
|
||||
}, [searchParams, setSearchParams]);
|
||||
|
||||
const selectedTool = toolRegistry[selectedToolKey];
|
||||
|
||||
// Tool component rendering
|
||||
@ -154,130 +210,144 @@ export default function HomePage() {
|
||||
return <div>Tool not found</div>;
|
||||
}
|
||||
|
||||
// Pass appropriate props based on tool type
|
||||
if (selectedToolKey === "split") {
|
||||
return React.createElement(selectedTool.component, {
|
||||
file: pdfFile,
|
||||
setPdfFile,
|
||||
downloadUrl,
|
||||
setDownloadUrl,
|
||||
// Tool-specific params and update function
|
||||
params: splitParams,
|
||||
updateParams: updateSplitParams
|
||||
});
|
||||
}
|
||||
|
||||
// For other tools, pass standard props
|
||||
// Pass only the necessary props
|
||||
return React.createElement(selectedTool.component, {
|
||||
file: pdfFile,
|
||||
setPdfFile,
|
||||
files,
|
||||
setFiles,
|
||||
downloadUrl,
|
||||
setDownloadUrl,
|
||||
params: toolParams,
|
||||
updateParams,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Group align="flex-start" gap={0} style={{ minHeight: "100vh" }}>
|
||||
{/* Left: Tool Picker */}
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={handleToolSelect}
|
||||
toolRegistry={toolRegistry}
|
||||
/>
|
||||
{sidebarsVisible && (
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={handleToolSelect}
|
||||
toolRegistry={toolRegistry}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Middle: Main View (Viewer, Editor, Manager) */}
|
||||
<Box
|
||||
style={{
|
||||
width: "calc(100vw - 220px - 380px)",
|
||||
marginLeft: 220,
|
||||
marginRight: 380,
|
||||
padding: 24,
|
||||
background: "#fff",
|
||||
position: "relative",
|
||||
minHeight: "100vh",
|
||||
width: sidebarsVisible
|
||||
? "calc(100vw - 220px - 380px)"
|
||||
: "100vw",
|
||||
marginLeft: sidebarsVisible ? 220 : 0,
|
||||
marginRight: sidebarsVisible ? 380 : 0,
|
||||
position: "relative", // <-- important for absolute overlay
|
||||
height: "100vh",
|
||||
overflowY: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
transition: "all 0.3s",
|
||||
}}
|
||||
>
|
||||
<Center>
|
||||
<Paper
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
p={4}
|
||||
style={{
|
||||
display: "inline-block",
|
||||
marginTop: 8,
|
||||
marginBottom: 24,
|
||||
background: "#f8f9fa",
|
||||
zIndex: 10,
|
||||
}}
|
||||
>
|
||||
<SegmentedControl
|
||||
data={VIEW_OPTIONS}
|
||||
value={currentView}
|
||||
onChange={setCurrentView} // Using the state setter directly
|
||||
color="blue"
|
||||
radius="xl"
|
||||
size="md"
|
||||
/>
|
||||
</Paper>
|
||||
</Center>
|
||||
<Box>
|
||||
{(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
|
||||
<FileManager
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setPdfFile={setPdfFile}
|
||||
setCurrentView={setCurrentView}
|
||||
/>
|
||||
) : currentView === "viewer" ? (
|
||||
<Viewer
|
||||
pdfFile={pdfFile}
|
||||
setPdfFile={setPdfFile}
|
||||
/>
|
||||
) : currentView === "pageEditor" ? (
|
||||
<PageEditor
|
||||
file={pdfFile}
|
||||
setFile={setPdfFile}
|
||||
downloadUrl={downloadUrl}
|
||||
setDownloadUrl={setDownloadUrl}
|
||||
/>
|
||||
) : (
|
||||
<FileManager
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setPdfFile={setPdfFile}
|
||||
setCurrentView={setCurrentView}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{/* Overlayed View Switcher */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
zIndex: 30,
|
||||
}}
|
||||
>
|
||||
<div style={{ pointerEvents: "auto" }}>
|
||||
<SegmentedControl
|
||||
data={VIEW_OPTIONS}
|
||||
value={currentView}
|
||||
onChange={setCurrentView}
|
||||
color="blue"
|
||||
radius="xl"
|
||||
size="md"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Main content area with matching Paper */}
|
||||
<Paper
|
||||
radius="0 0 xl xl"
|
||||
shadow="sm"
|
||||
p={0}
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
marginTop: 0,
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Box style={{ flex: 1, minHeight: 0 }}>
|
||||
{(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
|
||||
<FileManager
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setPdfFile={setPdfFile}
|
||||
setCurrentView={setCurrentView}
|
||||
/>
|
||||
) : currentView === "viewer" ? (
|
||||
<Viewer
|
||||
pdfFile={pdfFile}
|
||||
setPdfFile={setPdfFile}
|
||||
sidebarsVisible={sidebarsVisible}
|
||||
setSidebarsVisible={setSidebarsVisible}
|
||||
/>
|
||||
) : currentView === "pageEditor" ? (
|
||||
<PageEditor
|
||||
file={pdfFile}
|
||||
setFile={setPdfFile}
|
||||
downloadUrl={downloadUrl}
|
||||
setDownloadUrl={setDownloadUrl}
|
||||
/>
|
||||
) : (
|
||||
<FileManager
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
setPdfFile={setPdfFile}
|
||||
setCurrentView={setCurrentView}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
{/* Right: Tool Interaction */}
|
||||
<Box
|
||||
style={{
|
||||
width: 380,
|
||||
background: "#f8f9fa",
|
||||
borderLeft: "1px solid #e9ecef",
|
||||
minHeight: "100vh",
|
||||
padding: 24,
|
||||
gap: 16,
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: 100,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
{sidebarsVisible && (
|
||||
<Box
|
||||
style={{
|
||||
width: 380,
|
||||
borderLeft: "1px solid #e9ecef",
|
||||
height: "100vh",
|
||||
padding: 24,
|
||||
gap: 16,
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
{selectedTool && selectedTool.component && (
|
||||
<>
|
||||
{renderTool()}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
size="xs"
|
||||
style={{ position: "absolute", top: 16, right: 16, zIndex: 200 }}
|
||||
onClick={() => setSidebarsVisible(v => !v)}
|
||||
>
|
||||
{selectedTool && selectedTool.component && (
|
||||
<>
|
||||
{renderTool()}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
{sidebarsVisible ? "Hide Sidebars" : "Show Sidebars"}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
|
||||
|
||||
export interface CompressProps {
|
||||
@ -12,6 +13,9 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
setDownloadUrl,
|
||||
setLoading,
|
||||
}) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
|
||||
const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
|
||||
const [compressionLevel, setCompressionLevel] = useState<number>(5);
|
||||
const [grayscale, setGrayscale] = useState<boolean>(false);
|
||||
@ -56,8 +60,8 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Paper shadow="xs" p="md" radius="md" withBorder>
|
||||
<Stack>
|
||||
<Text fw={500} mb={4}>Select files to compress:</Text>
|
||||
<Stack gap={4}>
|
||||
@ -118,7 +122,6 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
|
||||
Compress Selected PDF{selected.filter(Boolean).length > 1 ? "s" : ""}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,12 +1,24 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
export interface MergePdfPanelProps {
|
||||
files: File[];
|
||||
setDownloadUrl: (url: string) => void;
|
||||
params: {
|
||||
order: string;
|
||||
removeDuplicates: boolean;
|
||||
};
|
||||
updateParams: (newParams: Partial<MergePdfPanelProps["params"]>) => void;
|
||||
}
|
||||
|
||||
const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl }) => {
|
||||
const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
||||
files,
|
||||
setDownloadUrl,
|
||||
params,
|
||||
updateParams,
|
||||
}) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
|
||||
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -59,8 +71,9 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl })
|
||||
|
||||
const selectedCount = selectedFiles.filter(Boolean).length;
|
||||
|
||||
const { order, removeDuplicates } = params;
|
||||
|
||||
return (
|
||||
<Paper shadow="xs" radius="md" p="md" withBorder>
|
||||
<Stack>
|
||||
<Text fw={500} size="lg">Merge PDFs</Text>
|
||||
<Stack gap={4}>
|
||||
@ -104,8 +117,12 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl })
|
||||
Download Merged PDF
|
||||
</Button>
|
||||
)}
|
||||
<Checkbox
|
||||
label="Remove Duplicates"
|
||||
checked={removeDuplicates}
|
||||
onChange={() => updateParams({ removeDuplicates: !removeDuplicates })}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,9 @@ import {
|
||||
Checkbox,
|
||||
Notification,
|
||||
Stack,
|
||||
Paper,
|
||||
} from "@mantine/core";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
|
||||
export interface SplitPdfPanelProps {
|
||||
@ -26,7 +28,7 @@ export interface SplitPdfPanelProps {
|
||||
includeMetadata: boolean;
|
||||
allowDuplicates: boolean;
|
||||
};
|
||||
updateParams: (newParams: Partial<SplitPdfPanelProps['params']>) => void;
|
||||
updateParams: (newParams: Partial<SplitPdfPanelProps["params"]>) => void;
|
||||
}
|
||||
|
||||
const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
@ -36,16 +38,26 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
params,
|
||||
updateParams,
|
||||
}) => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [status, setStatus] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
mode, pages, hDiv, vDiv, merge,
|
||||
splitType, splitValue, bookmarkLevel,
|
||||
includeMetadata, allowDuplicates
|
||||
mode,
|
||||
pages,
|
||||
hDiv,
|
||||
vDiv,
|
||||
merge,
|
||||
splitType,
|
||||
splitValue,
|
||||
bookmarkLevel,
|
||||
includeMetadata,
|
||||
allowDuplicates,
|
||||
} = params;
|
||||
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!file) {
|
||||
@ -109,123 +121,123 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="sm" mb={16}>
|
||||
<Select
|
||||
label="Split Mode"
|
||||
value={mode}
|
||||
onChange={(v) => v && updateParams({ mode: v })}
|
||||
data={[
|
||||
{ value: "byPages", label: "Split by Pages (e.g. 1,3,5-10)" },
|
||||
{ value: "bySections", label: "Split by Grid Sections" },
|
||||
{ value: "bySizeOrCount", label: "Split by Size or Count" },
|
||||
{ value: "byChapters", label: "Split by Chapters" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{mode === "byPages" && (
|
||||
<TextInput
|
||||
label="Pages"
|
||||
placeholder="e.g. 1,3,5-10"
|
||||
value={pages}
|
||||
onChange={(e) => updateParams({ pages: e.target.value })}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="sm" mb={16}>
|
||||
<Select
|
||||
label="Split Mode"
|
||||
value={mode}
|
||||
onChange={(v) => v && updateParams({ mode: v })}
|
||||
data={[
|
||||
{ value: "byPages", label: "Split by Pages (e.g. 1,3,5-10)" },
|
||||
{ value: "bySections", label: "Split by Grid Sections" },
|
||||
{ value: "bySizeOrCount", label: "Split by Size or Count" },
|
||||
{ value: "byChapters", label: "Split by Chapters" },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === "bySections" && (
|
||||
<Stack gap="sm">
|
||||
{mode === "byPages" && (
|
||||
<TextInput
|
||||
label="Horizontal Divisions"
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
value={hDiv}
|
||||
onChange={(e) => updateParams({ hDiv: e.target.value })}
|
||||
label="Pages"
|
||||
placeholder="e.g. 1,3,5-10"
|
||||
value={pages}
|
||||
onChange={(e) => updateParams({ pages: e.target.value })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Vertical Divisions"
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
value={vDiv}
|
||||
onChange={(e) => updateParams({ vDiv: e.target.value })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Merge sections into one PDF"
|
||||
checked={merge}
|
||||
onChange={(e) => updateParams({ merge: e.currentTarget.checked })}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
)}
|
||||
|
||||
{mode === "bySizeOrCount" && (
|
||||
<Stack gap="sm">
|
||||
<Select
|
||||
label="Split Type"
|
||||
value={splitType}
|
||||
onChange={(v) => v && updateParams({ splitType: v })}
|
||||
data={[
|
||||
{ value: "size", label: "By Size" },
|
||||
{ value: "pages", label: "By Page Count" },
|
||||
{ value: "docs", label: "By Document Count" },
|
||||
]}
|
||||
/>
|
||||
<TextInput
|
||||
label="Split Value"
|
||||
placeholder="e.g. 10MB or 5 pages"
|
||||
value={splitValue}
|
||||
onChange={(e) => updateParams({ splitValue: e.target.value })}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{mode === "bySections" && (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Horizontal Divisions"
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
value={hDiv}
|
||||
onChange={(e) => updateParams({ hDiv: e.target.value })}
|
||||
/>
|
||||
<TextInput
|
||||
label="Vertical Divisions"
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
value={vDiv}
|
||||
onChange={(e) => updateParams({ vDiv: e.target.value })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Merge sections into one PDF"
|
||||
checked={merge}
|
||||
onChange={(e) => updateParams({ merge: e.currentTarget.checked })}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{mode === "byChapters" && (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Bookmark Level"
|
||||
type="number"
|
||||
value={bookmarkLevel}
|
||||
onChange={(e) => updateParams({ bookmarkLevel: e.target.value })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Include Metadata"
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => updateParams({ includeMetadata: e.currentTarget.checked })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Allow Duplicate Bookmarks"
|
||||
checked={allowDuplicates}
|
||||
onChange={(e) => updateParams({ allowDuplicates: e.currentTarget.checked })}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{mode === "bySizeOrCount" && (
|
||||
<Stack gap="sm">
|
||||
<Select
|
||||
label="Split Type"
|
||||
value={splitType}
|
||||
onChange={(v) => v && updateParams({ splitType: v })}
|
||||
data={[
|
||||
{ value: "size", label: "By Size" },
|
||||
{ value: "pages", label: "By Page Count" },
|
||||
{ value: "docs", label: "By Document Count" },
|
||||
]}
|
||||
/>
|
||||
<TextInput
|
||||
label="Split Value"
|
||||
placeholder="e.g. 10MB or 5 pages"
|
||||
value={splitValue}
|
||||
onChange={(e) => updateParams({ splitValue: e.target.value })}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Button type="submit" loading={isLoading} fullWidth>
|
||||
{isLoading ? "Processing..." : "Split PDF"}
|
||||
</Button>
|
||||
{mode === "byChapters" && (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label="Bookmark Level"
|
||||
type="number"
|
||||
value={bookmarkLevel}
|
||||
onChange={(e) => updateParams({ bookmarkLevel: e.target.value })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Include Metadata"
|
||||
checked={includeMetadata}
|
||||
onChange={(e) => updateParams({ includeMetadata: e.currentTarget.checked })}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Allow Duplicate Bookmarks"
|
||||
checked={allowDuplicates}
|
||||
onChange={(e) => updateParams({ allowDuplicates: e.currentTarget.checked })}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{status && <p className="text-xs text-gray-600">{status}</p>}
|
||||
|
||||
{errorMessage && (
|
||||
<Notification color="red" title="Error" onClose={() => setErrorMessage(null)}>
|
||||
{errorMessage}
|
||||
</Notification>
|
||||
)}
|
||||
|
||||
{status === "Download ready." && downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
download="split_output.zip"
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
>
|
||||
Download Split PDF
|
||||
<Button type="submit" loading={isLoading} fullWidth>
|
||||
{isLoading ? "Processing..." : "Split PDF"}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
{status && <p className="text-xs text-gray-600">{status}</p>}
|
||||
|
||||
{errorMessage && (
|
||||
<Notification color="red" title="Error" onClose={() => setErrorMessage(null)}>
|
||||
{errorMessage}
|
||||
</Notification>
|
||||
)}
|
||||
|
||||
{status === "Download ready." && downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
download="split_output.zip"
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
>
|
||||
Download Split PDF
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user