mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-11-01 01:21:18 +01: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 './index.css';
 | 
				
			||||||
import HomePage from './pages/HomePage';
 | 
					import HomePage from './pages/HomePage';
 | 
				
			||||||
import SplitPdfPanel from './tools/Split';
 | 
					 | 
				
			||||||
import reportWebVitals from './reportWebVitals';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function App() {
 | 
					export default function App({ colorScheme, toggleColorScheme }) {
 | 
				
			||||||
  return (
 | 
					  return <HomePage colorScheme={colorScheme} toggleColorScheme={toggleColorScheme} />;
 | 
				
			||||||
    <Routes>
 | 
					 | 
				
			||||||
      <Route path="/" element={<HomePage />} />
 | 
					 | 
				
			||||||
      <Route path="/split" element={<SplitPdfPanel />} />
 | 
					 | 
				
			||||||
    </Routes>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
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 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 { Dropzone, MIME_TYPES } from "@mantine/dropzone";
 | 
				
			||||||
import { GlobalWorkerOptions, getDocument } from "pdfjs-dist";
 | 
					import { GlobalWorkerOptions, getDocument } from "pdfjs-dist";
 | 
				
			||||||
 | 
					import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GlobalWorkerOptions.workerSrc =
 | 
					GlobalWorkerOptions.workerSrc =
 | 
				
			||||||
  (import.meta as any).env?.PUBLIC_URL
 | 
					  (import.meta as any).env?.PUBLIC_URL
 | 
				
			||||||
@ -93,7 +94,15 @@ function FileCard({ file, onRemove, onDoubleClick }: FileCardProps) {
 | 
				
			|||||||
          {thumb ? (
 | 
					          {thumb ? (
 | 
				
			||||||
            <Image src={thumb} alt="PDF thumbnail" height={110} width={80} fit="contain" radius="sm" />
 | 
					            <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>
 | 
					        </Box>
 | 
				
			||||||
        <Text fw={500} size="sm" lineClamp={1} ta="center">
 | 
					        <Text fw={500} size="sm" lineClamp={1} ta="center">
 | 
				
			||||||
@ -145,21 +154,22 @@ const FileManager: React.FC<FileManagerProps> = ({
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  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
 | 
					      <Dropzone
 | 
				
			||||||
        onDrop={handleDrop}
 | 
					        onDrop={handleDrop}
 | 
				
			||||||
        accept={[MIME_TYPES.pdf]}
 | 
					        accept={[MIME_TYPES.pdf]}
 | 
				
			||||||
        multiple={allowMultiple}
 | 
					        multiple={allowMultiple}
 | 
				
			||||||
        maxSize={20 * 1024 * 1024}
 | 
					        maxSize={20 * 1024 * 1024}
 | 
				
			||||||
        style={{
 | 
					        style={{
 | 
				
			||||||
 | 
					          marginTop: 16,
 | 
				
			||||||
          marginBottom: 16,
 | 
					          marginBottom: 16,
 | 
				
			||||||
          border: "2px dashed rgb(202, 202, 202)",
 | 
					          border: "2px dashed rgb(202, 202, 202)",
 | 
				
			||||||
          background: "#f8fafc",
 | 
					 | 
				
			||||||
          borderRadius: 8,
 | 
					          borderRadius: 8,
 | 
				
			||||||
          minHeight: 120,
 | 
					          minHeight: 120,
 | 
				
			||||||
          display: "flex",
 | 
					          display: "flex",
 | 
				
			||||||
          alignItems: "center",
 | 
					          alignItems: "center",
 | 
				
			||||||
          justifyContent: "center",
 | 
					          justifyContent: "center",
 | 
				
			||||||
 | 
					          width:"90%"
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <Group justify="center" gap="xl" style={{ pointerEvents: "none" }}>
 | 
					        <Group justify="center" gap="xl" style={{ pointerEvents: "none" }}>
 | 
				
			||||||
 | 
				
			|||||||
@ -27,7 +27,6 @@ const ToolPicker: React.FC<ToolPickerProps> = ({ selectedToolKey, onSelect, tool
 | 
				
			|||||||
    <Box
 | 
					    <Box
 | 
				
			||||||
      style={{
 | 
					      style={{
 | 
				
			||||||
        width: 220,
 | 
					        width: 220,
 | 
				
			||||||
        background: "#f8f9fa",
 | 
					 | 
				
			||||||
        borderRight: "1px solid #e9ecef",
 | 
					        borderRight: "1px solid #e9ecef",
 | 
				
			||||||
        minHeight: "100vh",
 | 
					        minHeight: "100vh",
 | 
				
			||||||
        padding: 16,
 | 
					        padding: 16,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,121 @@
 | 
				
			|||||||
import React, { useEffect, useState } from "react";
 | 
					import React, { useEffect, useState, useRef } from "react";
 | 
				
			||||||
import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group } from "@mantine/core";
 | 
					import { Paper, Stack, Text, ScrollArea, Loader, Center, Button, Group, NumberInput, useMantineTheme } from "@mantine/core";
 | 
				
			||||||
import { getDocument, GlobalWorkerOptions } from "pdfjs-dist";
 | 
					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`;
 | 
					GlobalWorkerOptions.workerSrc = `${process.env.PUBLIC_URL}/pdf.worker.js`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ViewerProps {
 | 
					export interface ViewerProps {
 | 
				
			||||||
  pdfFile: { file: File; url: string } | null;
 | 
					  pdfFile: { file: File; url: string } | null;
 | 
				
			||||||
  setPdfFile: (file: { file: File; url: string } | null) => void;
 | 
					  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 [numPages, setNumPages] = useState<number>(0);
 | 
				
			||||||
  const [pageImages, setPageImages] = useState<string[]>([]);
 | 
					  const [pageImages, setPageImages] = useState<string[]>([]);
 | 
				
			||||||
  const [loading, setLoading] = useState<boolean>(false);
 | 
					  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(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    let cancelled = false;
 | 
					    let cancelled = false;
 | 
				
			||||||
@ -49,12 +152,31 @@ const Viewer: React.FC<ViewerProps> = ({ pdfFile, setPdfFile }) => {
 | 
				
			|||||||
    return () => { cancelled = true; };
 | 
					    return () => { cancelled = true; };
 | 
				
			||||||
  }, [pdfFile]);
 | 
					  }, [pdfFile]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const viewport = scrollAreaRef.current;
 | 
				
			||||||
 | 
					    if (!viewport) return;
 | 
				
			||||||
 | 
					    const handler = () => {
 | 
				
			||||||
 | 
					      handleScroll();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    viewport.addEventListener("scroll", handler);
 | 
				
			||||||
 | 
					    return () => viewport.removeEventListener("scroll", handler);
 | 
				
			||||||
 | 
					  }, [pageImages]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  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 ? (
 | 
					      {!pdfFile ? (
 | 
				
			||||||
        <Center style={{ flex: 1 }}>
 | 
					        <Center style={{ flex: 1 }}>
 | 
				
			||||||
          <Stack align="center">
 | 
					          <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
 | 
					            <Button
 | 
				
			||||||
              component="label"
 | 
					              component="label"
 | 
				
			||||||
              variant="outline"
 | 
					              variant="outline"
 | 
				
			||||||
@ -81,39 +203,222 @@ const Viewer: React.FC<ViewerProps> = ({ pdfFile, setPdfFile }) => {
 | 
				
			|||||||
          <Loader size="lg" />
 | 
					          <Loader size="lg" />
 | 
				
			||||||
        </Center>
 | 
					        </Center>
 | 
				
			||||||
      ) : (
 | 
					      ) : (
 | 
				
			||||||
        <ScrollArea style={{ flex: 1, height: "100%" }}>
 | 
					        <ScrollArea
 | 
				
			||||||
          <Stack gap="xl" align="center">
 | 
					          style={{ flex: 1, height: "100%", position: "relative"}}
 | 
				
			||||||
 | 
					          viewportRef={scrollAreaRef}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <Stack gap="xl" align="center" >
 | 
				
			||||||
            {pageImages.length === 0 && (
 | 
					            {pageImages.length === 0 && (
 | 
				
			||||||
              <Text color="dimmed">No pages to display.</Text>
 | 
					              <Text color="dimmed">No pages to display.</Text>
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
            {pageImages.map((img, idx) => (
 | 
					            {dualPage
 | 
				
			||||||
              <img
 | 
					              ? Array.from({ length: Math.ceil(pageImages.length / 2) }).map((_, i) => (
 | 
				
			||||||
                key={idx}
 | 
					                  <Group key={i} gap="md" align="flex-start" style={{ width: "100%", justifyContent: "center" }}>
 | 
				
			||||||
                src={img}
 | 
					                    <img
 | 
				
			||||||
                alt={`Page ${idx + 1}`}
 | 
					                      ref={el => { pageRefs.current[i * 2] = el; }}
 | 
				
			||||||
                style={{
 | 
					                      src={pageImages[i * 2]}
 | 
				
			||||||
                  width: "100%",
 | 
					                      alt={`Page ${i * 2 + 1}`}
 | 
				
			||||||
                  maxWidth: 700,
 | 
					                      style={{
 | 
				
			||||||
                  boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
 | 
					                        width: `${100 * zoom}%`,
 | 
				
			||||||
                  borderRadius: 8,
 | 
					                        maxWidth: 700 * zoom,
 | 
				
			||||||
                  background: "#fff"
 | 
					                        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},
 | 
				
			||||||
                }}
 | 
					                }}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            ))}
 | 
					              <span style={{ fontWeight: 500, fontSize: 16 }}>
 | 
				
			||||||
          </Stack>
 | 
					                / {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>
 | 
					        </ScrollArea>
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
      {pdfFile && (
 | 
					
 | 
				
			||||||
        <Group justify="flex-end" mt="md">
 | 
					 | 
				
			||||||
          <Button
 | 
					 | 
				
			||||||
            variant="light"
 | 
					 | 
				
			||||||
            color="red"
 | 
					 | 
				
			||||||
            onClick={() => setPdfFile(null)}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            Close PDF
 | 
					 | 
				
			||||||
          </Button>
 | 
					 | 
				
			||||||
        </Group>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </Paper>
 | 
					    </Paper>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ import './index.css';
 | 
				
			|||||||
import App from './App';
 | 
					import App from './App';
 | 
				
			||||||
import reportWebVitals from './reportWebVitals';
 | 
					import reportWebVitals from './reportWebVitals';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const root = ReactDOM.createRoot(document.getElementById('root')); // Finds the root DOM element
 | 
					const root = ReactDOM.createRoot(document.getElementById('root')); // Finds the root DOM element
 | 
				
			||||||
root.render(
 | 
					root.render(
 | 
				
			||||||
  <React.StrictMode>
 | 
					  <React.StrictMode>
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
 | 
				
			|||||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
 | 
					import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
 | 
				
			||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
 | 
					import VisibilityIcon from "@mui/icons-material/Visibility";
 | 
				
			||||||
import EditNoteIcon from "@mui/icons-material/EditNote";
 | 
					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 ToolPicker from "../components/ToolPicker";
 | 
				
			||||||
import FileManager from "../components/FileManager";
 | 
					import FileManager from "../components/FileManager";
 | 
				
			||||||
@ -15,6 +15,8 @@ import CompressPdfPanel from "../tools/Compress";
 | 
				
			|||||||
import MergePdfPanel from "../tools/Merge";
 | 
					import MergePdfPanel from "../tools/Merge";
 | 
				
			||||||
import PageEditor from "../components/PageEditor";
 | 
					import PageEditor from "../components/PageEditor";
 | 
				
			||||||
import Viewer from "../components/Viewer";
 | 
					import Viewer from "../components/Viewer";
 | 
				
			||||||
 | 
					import DarkModeIcon from '@mui/icons-material/DarkMode';
 | 
				
			||||||
 | 
					import LightModeIcon from '@mui/icons-material/LightMode';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ToolRegistryEntry = {
 | 
					type ToolRegistryEntry = {
 | 
				
			||||||
  icon: React.ReactNode;
 | 
					  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() {
 | 
					export default function HomePage() {
 | 
				
			||||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
					  const [searchParams, setSearchParams] = useSearchParams();
 | 
				
			||||||
 | 
					  const theme = useMantineTheme();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Core app state
 | 
					  // Core app state
 | 
				
			||||||
  const [selectedToolKey, setSelectedToolKey] = useState<string>(searchParams.get("tool") || "split");
 | 
					  const [selectedToolKey, setSelectedToolKey] = useState<string>(searchParams.get("tool") || "split");
 | 
				
			||||||
@ -69,28 +161,37 @@ export default function HomePage() {
 | 
				
			|||||||
  const [pdfFile, setPdfFile] = useState<any>(null);
 | 
					  const [pdfFile, setPdfFile] = useState<any>(null);
 | 
				
			||||||
  const [files, setFiles] = useState<any[]>([]);
 | 
					  const [files, setFiles] = useState<any[]>([]);
 | 
				
			||||||
  const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
 | 
					  const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
 | 
				
			||||||
 | 
					  const [sidebarsVisible, setSidebarsVisible] = useState(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Tool-specific parameters
 | 
					  const toolParams = getToolParams(selectedToolKey, searchParams);
 | 
				
			||||||
  const [splitParams, setSplitParams] = useState({
 | 
					
 | 
				
			||||||
    mode: searchParams.get("splitMode") || "byPages",
 | 
					  const updateParams = (newParams: any) =>
 | 
				
			||||||
    pages: searchParams.get("pages") || "",
 | 
					    updateToolParams(selectedToolKey, searchParams, setSearchParams, newParams);
 | 
				
			||||||
    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",
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Update URL when core state changes
 | 
					  // Update URL when core state changes
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const params = new URLSearchParams(searchParams);
 | 
					    const params = new URLSearchParams(searchParams);
 | 
				
			||||||
    params.set("tool", selectedToolKey);
 | 
					
 | 
				
			||||||
    params.set("view", currentView);
 | 
					    // Remove all tool-specific params except for the current tool
 | 
				
			||||||
    setSearchParams(params, { replace: true });
 | 
					    Object.entries(TOOL_PARAMS).forEach(([tool, keys]) => {
 | 
				
			||||||
  }, [selectedToolKey, currentView, setSearchParams]);
 | 
					      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
 | 
					  // Handle tool selection
 | 
				
			||||||
  const handleToolSelect = useCallback(
 | 
					  const handleToolSelect = useCallback(
 | 
				
			||||||
@ -101,51 +202,6 @@ export default function HomePage() {
 | 
				
			|||||||
    [toolRegistry]
 | 
					    [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];
 | 
					  const selectedTool = toolRegistry[selectedToolKey];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Tool component rendering
 | 
					  // Tool component rendering
 | 
				
			||||||
@ -154,130 +210,144 @@ export default function HomePage() {
 | 
				
			|||||||
      return <div>Tool not found</div>;
 | 
					      return <div>Tool not found</div>;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Pass appropriate props based on tool type
 | 
					    // Pass only the necessary props
 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
    return React.createElement(selectedTool.component, {
 | 
					    return React.createElement(selectedTool.component, {
 | 
				
			||||||
      file: pdfFile,
 | 
					 | 
				
			||||||
      setPdfFile,
 | 
					 | 
				
			||||||
      files,
 | 
					      files,
 | 
				
			||||||
      setFiles,
 | 
					 | 
				
			||||||
      downloadUrl,
 | 
					 | 
				
			||||||
      setDownloadUrl,
 | 
					      setDownloadUrl,
 | 
				
			||||||
 | 
					      params: toolParams,
 | 
				
			||||||
 | 
					      updateParams,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Group align="flex-start" gap={0} style={{ minHeight: "100vh" }}>
 | 
					    <Group align="flex-start" gap={0} style={{ minHeight: "100vh" }}>
 | 
				
			||||||
      {/* Left: Tool Picker */}
 | 
					      {/* Left: Tool Picker */}
 | 
				
			||||||
      <ToolPicker
 | 
					      {sidebarsVisible && (
 | 
				
			||||||
        selectedToolKey={selectedToolKey}
 | 
					        <ToolPicker
 | 
				
			||||||
        onSelect={handleToolSelect}
 | 
					          selectedToolKey={selectedToolKey}
 | 
				
			||||||
        toolRegistry={toolRegistry}
 | 
					          onSelect={handleToolSelect}
 | 
				
			||||||
      />
 | 
					          toolRegistry={toolRegistry}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {/* Middle: Main View (Viewer, Editor, Manager) */}
 | 
					      {/* Middle: Main View (Viewer, Editor, Manager) */}
 | 
				
			||||||
      <Box
 | 
					      <Box
 | 
				
			||||||
        style={{
 | 
					        style={{
 | 
				
			||||||
          width: "calc(100vw - 220px - 380px)",
 | 
					          width: sidebarsVisible
 | 
				
			||||||
          marginLeft: 220,
 | 
					            ? "calc(100vw - 220px - 380px)"
 | 
				
			||||||
          marginRight: 380,
 | 
					            : "100vw",
 | 
				
			||||||
          padding: 24,
 | 
					          marginLeft: sidebarsVisible ? 220 : 0,
 | 
				
			||||||
          background: "#fff",
 | 
					          marginRight: sidebarsVisible ? 380 : 0,
 | 
				
			||||||
          position: "relative",
 | 
					          position: "relative", // <-- important for absolute overlay
 | 
				
			||||||
          minHeight: "100vh",
 | 
					 | 
				
			||||||
          height: "100vh",
 | 
					          height: "100vh",
 | 
				
			||||||
          overflowY: "auto",
 | 
					          display: "flex",
 | 
				
			||||||
 | 
					          flexDirection: "column",
 | 
				
			||||||
 | 
					          transition: "all 0.3s",
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <Center>
 | 
					        {/* Overlayed View Switcher */}
 | 
				
			||||||
          <Paper
 | 
					        <div
 | 
				
			||||||
            radius="xl"
 | 
					          style={{
 | 
				
			||||||
            shadow="sm"
 | 
					            position: "absolute",
 | 
				
			||||||
            p={4}
 | 
					            left: 0,
 | 
				
			||||||
            style={{
 | 
					            width: "100%",
 | 
				
			||||||
              display: "inline-block",
 | 
					            display: "flex",
 | 
				
			||||||
              marginTop: 8,
 | 
					            justifyContent: "center",
 | 
				
			||||||
              marginBottom: 24,
 | 
					            zIndex: 30,
 | 
				
			||||||
              background: "#f8f9fa",
 | 
					          }}
 | 
				
			||||||
              zIndex: 10,
 | 
					        >
 | 
				
			||||||
            }}
 | 
					          <div style={{ pointerEvents: "auto" }}>
 | 
				
			||||||
          >
 | 
					               <SegmentedControl
 | 
				
			||||||
            <SegmentedControl
 | 
					                data={VIEW_OPTIONS}
 | 
				
			||||||
              data={VIEW_OPTIONS}
 | 
					                value={currentView}
 | 
				
			||||||
              value={currentView}
 | 
					                onChange={setCurrentView}
 | 
				
			||||||
              onChange={setCurrentView} // Using the state setter directly
 | 
					                color="blue"
 | 
				
			||||||
              color="blue"
 | 
					                radius="xl"
 | 
				
			||||||
              radius="xl"
 | 
					                size="md"
 | 
				
			||||||
              size="md"
 | 
					                fullWidth
 | 
				
			||||||
            />
 | 
					              />
 | 
				
			||||||
          </Paper>
 | 
					          </div>
 | 
				
			||||||
        </Center>
 | 
					        </div>
 | 
				
			||||||
        <Box>
 | 
					        {/* Main content area with matching Paper */}
 | 
				
			||||||
          {(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
 | 
					        <Paper
 | 
				
			||||||
            <FileManager
 | 
					          radius="0 0 xl xl"
 | 
				
			||||||
              files={files}
 | 
					          shadow="sm"
 | 
				
			||||||
              setFiles={setFiles}
 | 
					          p={0}
 | 
				
			||||||
              setPdfFile={setPdfFile}
 | 
					          style={{
 | 
				
			||||||
              setCurrentView={setCurrentView}
 | 
					            flex: 1,
 | 
				
			||||||
            />
 | 
					            minHeight: 0,
 | 
				
			||||||
          ) : currentView === "viewer" ? (
 | 
					            marginTop: 0,
 | 
				
			||||||
            <Viewer
 | 
					            boxSizing: "border-box",
 | 
				
			||||||
              pdfFile={pdfFile}
 | 
					            overflow: "hidden",
 | 
				
			||||||
              setPdfFile={setPdfFile}
 | 
					            display: "flex",
 | 
				
			||||||
            />
 | 
					            flexDirection: "column",
 | 
				
			||||||
          ) : currentView === "pageEditor" ? (
 | 
					          }}
 | 
				
			||||||
            <PageEditor
 | 
					        >
 | 
				
			||||||
              file={pdfFile}
 | 
					          <Box style={{ flex: 1, minHeight: 0 }}>
 | 
				
			||||||
              setFile={setPdfFile}
 | 
					            {(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
 | 
				
			||||||
              downloadUrl={downloadUrl}
 | 
					              <FileManager
 | 
				
			||||||
              setDownloadUrl={setDownloadUrl}
 | 
					                files={files}
 | 
				
			||||||
            />
 | 
					                setFiles={setFiles}
 | 
				
			||||||
          ) : (
 | 
					                setPdfFile={setPdfFile}
 | 
				
			||||||
            <FileManager
 | 
					                setCurrentView={setCurrentView}
 | 
				
			||||||
              files={files}
 | 
					              />
 | 
				
			||||||
              setFiles={setFiles}
 | 
					            ) : currentView === "viewer" ? (
 | 
				
			||||||
              setPdfFile={setPdfFile}
 | 
					              <Viewer
 | 
				
			||||||
              setCurrentView={setCurrentView}
 | 
					                pdfFile={pdfFile}
 | 
				
			||||||
            />
 | 
					                setPdfFile={setPdfFile}
 | 
				
			||||||
          )}
 | 
					                sidebarsVisible={sidebarsVisible}
 | 
				
			||||||
        </Box>
 | 
					                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>
 | 
					      </Box>
 | 
				
			||||||
      {/* Right: Tool Interaction */}
 | 
					      {/* Right: Tool Interaction */}
 | 
				
			||||||
      <Box
 | 
					      {sidebarsVisible && (
 | 
				
			||||||
        style={{
 | 
					        <Box
 | 
				
			||||||
          width: 380,
 | 
					          style={{
 | 
				
			||||||
          background: "#f8f9fa",
 | 
					            width: 380,
 | 
				
			||||||
          borderLeft: "1px solid #e9ecef",
 | 
					            borderLeft: "1px solid #e9ecef",
 | 
				
			||||||
          minHeight: "100vh",
 | 
					            height: "100vh",
 | 
				
			||||||
          padding: 24,
 | 
					            padding: 24,
 | 
				
			||||||
          gap: 16,
 | 
					            gap: 16,
 | 
				
			||||||
          position: "fixed",
 | 
					            position: "fixed",
 | 
				
			||||||
          right: 0,
 | 
					            right: 0,
 | 
				
			||||||
          top: 0,
 | 
					            top: 0,
 | 
				
			||||||
          bottom: 0,
 | 
					            bottom: 0,
 | 
				
			||||||
          zIndex: 100,
 | 
					            zIndex: 100,
 | 
				
			||||||
          overflowY: "auto",
 | 
					          }}
 | 
				
			||||||
        }}
 | 
					        >
 | 
				
			||||||
 | 
					          {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 && (
 | 
					        {sidebarsVisible ? "Hide Sidebars" : "Show Sidebars"}
 | 
				
			||||||
          <>
 | 
					      </Button>
 | 
				
			||||||
            {renderTool()}
 | 
					 | 
				
			||||||
          </>
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </Box>
 | 
					 | 
				
			||||||
    </Group>
 | 
					    </Group>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import React, { useState } from "react";
 | 
					import React, { useState } from "react";
 | 
				
			||||||
 | 
					import { useSearchParams } from "react-router-dom";
 | 
				
			||||||
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
 | 
					import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface CompressProps {
 | 
					export interface CompressProps {
 | 
				
			||||||
@ -12,6 +13,9 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
 | 
				
			|||||||
  setDownloadUrl,
 | 
					  setDownloadUrl,
 | 
				
			||||||
  setLoading,
 | 
					  setLoading,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
 | 
					  const [searchParams] = useSearchParams();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
 | 
					  const [selected, setSelected] = useState<boolean[]>(files.map(() => false));
 | 
				
			||||||
  const [compressionLevel, setCompressionLevel] = useState<number>(5);
 | 
					  const [compressionLevel, setCompressionLevel] = useState<number>(5);
 | 
				
			||||||
  const [grayscale, setGrayscale] = useState<boolean>(false);
 | 
					  const [grayscale, setGrayscale] = useState<boolean>(false);
 | 
				
			||||||
@ -56,8 +60,8 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Paper shadow="xs" p="md" radius="md" withBorder>
 | 
					 | 
				
			||||||
      <Stack>
 | 
					      <Stack>
 | 
				
			||||||
        <Text fw={500} mb={4}>Select files to compress:</Text>
 | 
					        <Text fw={500} mb={4}>Select files to compress:</Text>
 | 
				
			||||||
        <Stack gap={4}>
 | 
					        <Stack gap={4}>
 | 
				
			||||||
@ -118,7 +122,6 @@ const CompressPdfPanel: React.FC<CompressProps> = ({
 | 
				
			|||||||
          Compress Selected PDF{selected.filter(Boolean).length > 1 ? "s" : ""}
 | 
					          Compress Selected PDF{selected.filter(Boolean).length > 1 ? "s" : ""}
 | 
				
			||||||
        </Button>
 | 
					        </Button>
 | 
				
			||||||
      </Stack>
 | 
					      </Stack>
 | 
				
			||||||
    </Paper>
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,24 @@
 | 
				
			|||||||
import React, { useState, useEffect } from "react";
 | 
					import React, { useState, useEffect } from "react";
 | 
				
			||||||
import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core";
 | 
					import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core";
 | 
				
			||||||
 | 
					import { useSearchParams } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface MergePdfPanelProps {
 | 
					export interface MergePdfPanelProps {
 | 
				
			||||||
  files: File[];
 | 
					  files: File[];
 | 
				
			||||||
  setDownloadUrl: (url: string) => void;
 | 
					  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 [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
 | 
				
			||||||
  const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
 | 
					  const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
 | 
				
			||||||
  const [isLoading, setIsLoading] = useState(false);
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
@ -59,8 +71,9 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl })
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const selectedCount = selectedFiles.filter(Boolean).length;
 | 
					  const selectedCount = selectedFiles.filter(Boolean).length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { order, removeDuplicates } = params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Paper shadow="xs" radius="md" p="md" withBorder>
 | 
					 | 
				
			||||||
      <Stack>
 | 
					      <Stack>
 | 
				
			||||||
        <Text fw={500} size="lg">Merge PDFs</Text>
 | 
					        <Text fw={500} size="lg">Merge PDFs</Text>
 | 
				
			||||||
        <Stack gap={4}>
 | 
					        <Stack gap={4}>
 | 
				
			||||||
@ -104,8 +117,12 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl })
 | 
				
			|||||||
            Download Merged PDF
 | 
					            Download Merged PDF
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
 | 
					        <Checkbox
 | 
				
			||||||
 | 
					          label="Remove Duplicates"
 | 
				
			||||||
 | 
					          checked={removeDuplicates}
 | 
				
			||||||
 | 
					          onChange={() => updateParams({ removeDuplicates: !removeDuplicates })}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
      </Stack>
 | 
					      </Stack>
 | 
				
			||||||
    </Paper>
 | 
					 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,9 @@ import {
 | 
				
			|||||||
  Checkbox,
 | 
					  Checkbox,
 | 
				
			||||||
  Notification,
 | 
					  Notification,
 | 
				
			||||||
  Stack,
 | 
					  Stack,
 | 
				
			||||||
 | 
					  Paper,
 | 
				
			||||||
} from "@mantine/core";
 | 
					} from "@mantine/core";
 | 
				
			||||||
 | 
					import { useSearchParams } from "react-router-dom";
 | 
				
			||||||
import DownloadIcon from "@mui/icons-material/Download";
 | 
					import DownloadIcon from "@mui/icons-material/Download";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SplitPdfPanelProps {
 | 
					export interface SplitPdfPanelProps {
 | 
				
			||||||
@ -26,7 +28,7 @@ export interface SplitPdfPanelProps {
 | 
				
			|||||||
    includeMetadata: boolean;
 | 
					    includeMetadata: boolean;
 | 
				
			||||||
    allowDuplicates: boolean;
 | 
					    allowDuplicates: boolean;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  updateParams: (newParams: Partial<SplitPdfPanelProps['params']>) => void;
 | 
					  updateParams: (newParams: Partial<SplitPdfPanelProps["params"]>) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
 | 
					const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
 | 
				
			||||||
@ -36,16 +38,26 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
 | 
				
			|||||||
  params,
 | 
					  params,
 | 
				
			||||||
  updateParams,
 | 
					  updateParams,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
 | 
					  const [searchParams] = useSearchParams();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [status, setStatus] = useState("");
 | 
					  const [status, setStatus] = useState("");
 | 
				
			||||||
  const [isLoading, setIsLoading] = useState(false);
 | 
					  const [isLoading, setIsLoading] = useState(false);
 | 
				
			||||||
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
 | 
					  const [errorMessage, setErrorMessage] = useState<string | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    mode, pages, hDiv, vDiv, merge,
 | 
					    mode,
 | 
				
			||||||
    splitType, splitValue, bookmarkLevel,
 | 
					    pages,
 | 
				
			||||||
    includeMetadata, allowDuplicates
 | 
					    hDiv,
 | 
				
			||||||
 | 
					    vDiv,
 | 
				
			||||||
 | 
					    merge,
 | 
				
			||||||
 | 
					    splitType,
 | 
				
			||||||
 | 
					    splitValue,
 | 
				
			||||||
 | 
					    bookmarkLevel,
 | 
				
			||||||
 | 
					    includeMetadata,
 | 
				
			||||||
 | 
					    allowDuplicates,
 | 
				
			||||||
  } = params;
 | 
					  } = params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSubmit = async (e: React.FormEvent) => {
 | 
					  const handleSubmit = async (e: React.FormEvent) => {
 | 
				
			||||||
    e.preventDefault();
 | 
					    e.preventDefault();
 | 
				
			||||||
    if (!file) {
 | 
					    if (!file) {
 | 
				
			||||||
@ -109,123 +121,123 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <form onSubmit={handleSubmit}>
 | 
					      <form onSubmit={handleSubmit}>
 | 
				
			||||||
      <Stack gap="sm" mb={16}>
 | 
					        <Stack gap="sm" mb={16}>
 | 
				
			||||||
        <Select
 | 
					          <Select
 | 
				
			||||||
          label="Split Mode"
 | 
					            label="Split Mode"
 | 
				
			||||||
          value={mode}
 | 
					            value={mode}
 | 
				
			||||||
          onChange={(v) => v && updateParams({ mode: v })}
 | 
					            onChange={(v) => v && updateParams({ mode: v })}
 | 
				
			||||||
          data={[
 | 
					            data={[
 | 
				
			||||||
            { value: "byPages", label: "Split by Pages (e.g. 1,3,5-10)" },
 | 
					              { value: "byPages", label: "Split by Pages (e.g. 1,3,5-10)" },
 | 
				
			||||||
            { value: "bySections", label: "Split by Grid Sections" },
 | 
					              { value: "bySections", label: "Split by Grid Sections" },
 | 
				
			||||||
            { value: "bySizeOrCount", label: "Split by Size or Count" },
 | 
					              { value: "bySizeOrCount", label: "Split by Size or Count" },
 | 
				
			||||||
            { value: "byChapters", label: "Split by Chapters" },
 | 
					              { 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 })}
 | 
					 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {mode === "bySections" && (
 | 
					          {mode === "byPages" && (
 | 
				
			||||||
          <Stack gap="sm">
 | 
					 | 
				
			||||||
            <TextInput
 | 
					            <TextInput
 | 
				
			||||||
              label="Horizontal Divisions"
 | 
					              label="Pages"
 | 
				
			||||||
              type="number"
 | 
					              placeholder="e.g. 1,3,5-10"
 | 
				
			||||||
              min="0"
 | 
					              value={pages}
 | 
				
			||||||
              max="300"
 | 
					              onChange={(e) => updateParams({ pages: e.target.value })}
 | 
				
			||||||
              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 === "bySizeOrCount" && (
 | 
					          {mode === "bySections" && (
 | 
				
			||||||
          <Stack gap="sm">
 | 
					            <Stack gap="sm">
 | 
				
			||||||
            <Select
 | 
					              <TextInput
 | 
				
			||||||
              label="Split Type"
 | 
					                label="Horizontal Divisions"
 | 
				
			||||||
              value={splitType}
 | 
					                type="number"
 | 
				
			||||||
              onChange={(v) => v && updateParams({ splitType: v })}
 | 
					                min="0"
 | 
				
			||||||
              data={[
 | 
					                max="300"
 | 
				
			||||||
                { value: "size", label: "By Size" },
 | 
					                value={hDiv}
 | 
				
			||||||
                { value: "pages", label: "By Page Count" },
 | 
					                onChange={(e) => updateParams({ hDiv: e.target.value })}
 | 
				
			||||||
                { value: "docs", label: "By Document Count" },
 | 
					              />
 | 
				
			||||||
              ]}
 | 
					              <TextInput
 | 
				
			||||||
            />
 | 
					                label="Vertical Divisions"
 | 
				
			||||||
            <TextInput
 | 
					                type="number"
 | 
				
			||||||
              label="Split Value"
 | 
					                min="0"
 | 
				
			||||||
              placeholder="e.g. 10MB or 5 pages"
 | 
					                max="300"
 | 
				
			||||||
              value={splitValue}
 | 
					                value={vDiv}
 | 
				
			||||||
              onChange={(e) => updateParams({ splitValue: e.target.value })}
 | 
					                onChange={(e) => updateParams({ vDiv: e.target.value })}
 | 
				
			||||||
            />
 | 
					              />
 | 
				
			||||||
          </Stack>
 | 
					              <Checkbox
 | 
				
			||||||
        )}
 | 
					                label="Merge sections into one PDF"
 | 
				
			||||||
 | 
					                checked={merge}
 | 
				
			||||||
 | 
					                onChange={(e) => updateParams({ merge: e.currentTarget.checked })}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </Stack>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {mode === "byChapters" && (
 | 
					          {mode === "bySizeOrCount" && (
 | 
				
			||||||
          <Stack gap="sm">
 | 
					            <Stack gap="sm">
 | 
				
			||||||
            <TextInput
 | 
					              <Select
 | 
				
			||||||
              label="Bookmark Level"
 | 
					                label="Split Type"
 | 
				
			||||||
              type="number"
 | 
					                value={splitType}
 | 
				
			||||||
              value={bookmarkLevel}
 | 
					                onChange={(v) => v && updateParams({ splitType: v })}
 | 
				
			||||||
              onChange={(e) => updateParams({ bookmarkLevel: e.target.value })}
 | 
					                data={[
 | 
				
			||||||
            />
 | 
					                  { value: "size", label: "By Size" },
 | 
				
			||||||
            <Checkbox
 | 
					                  { value: "pages", label: "By Page Count" },
 | 
				
			||||||
              label="Include Metadata"
 | 
					                  { value: "docs", label: "By Document Count" },
 | 
				
			||||||
              checked={includeMetadata}
 | 
					                ]}
 | 
				
			||||||
              onChange={(e) => updateParams({ includeMetadata: e.currentTarget.checked })}
 | 
					              />
 | 
				
			||||||
            />
 | 
					              <TextInput
 | 
				
			||||||
            <Checkbox
 | 
					                label="Split Value"
 | 
				
			||||||
              label="Allow Duplicate Bookmarks"
 | 
					                placeholder="e.g. 10MB or 5 pages"
 | 
				
			||||||
              checked={allowDuplicates}
 | 
					                value={splitValue}
 | 
				
			||||||
              onChange={(e) => updateParams({ allowDuplicates: e.currentTarget.checked })}
 | 
					                onChange={(e) => updateParams({ splitValue: e.target.value })}
 | 
				
			||||||
            />
 | 
					              />
 | 
				
			||||||
          </Stack>
 | 
					            </Stack>
 | 
				
			||||||
        )}
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Button type="submit" loading={isLoading} fullWidth>
 | 
					          {mode === "byChapters" && (
 | 
				
			||||||
          {isLoading ? "Processing..." : "Split PDF"}
 | 
					            <Stack gap="sm">
 | 
				
			||||||
        </Button>
 | 
					              <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>}
 | 
					          <Button type="submit" loading={isLoading} fullWidth>
 | 
				
			||||||
 | 
					            {isLoading ? "Processing..." : "Split PDF"}
 | 
				
			||||||
        {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>
 | 
					          </Button>
 | 
				
			||||||
        )}
 | 
					
 | 
				
			||||||
      </Stack>
 | 
					          {status && <p className="text-xs text-gray-600">{status}</p>}
 | 
				
			||||||
    </form>
 | 
					
 | 
				
			||||||
 | 
					          {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…
	
		Reference in New Issue
	
	Block a user