2025-05-21 21:47:44 +01:00
|
|
|
import React, { useState, useCallback, useEffect } from "react";
|
|
|
|
import { useSearchParams } from "react-router-dom";
|
|
|
|
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
|
|
|
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
|
|
|
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";
|
2025-05-27 19:22:26 +01:00
|
|
|
import { Group, SegmentedControl, Paper, Center, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
2025-05-21 21:47:44 +01:00
|
|
|
|
|
|
|
import ToolPicker from "../components/ToolPicker";
|
|
|
|
import FileManager from "../components/FileManager";
|
|
|
|
import SplitPdfPanel from "../tools/Split";
|
|
|
|
import CompressPdfPanel from "../tools/Compress";
|
|
|
|
import MergePdfPanel from "../tools/Merge";
|
|
|
|
import PageEditor from "../components/PageEditor";
|
|
|
|
import Viewer from "../components/Viewer";
|
2025-05-27 19:22:26 +01:00
|
|
|
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
|
|
|
import LightModeIcon from '@mui/icons-material/LightMode';
|
2025-05-21 21:47:44 +01:00
|
|
|
|
|
|
|
type ToolRegistryEntry = {
|
|
|
|
icon: React.ReactNode;
|
|
|
|
name: string;
|
|
|
|
component: React.ComponentType<any>;
|
|
|
|
view: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
type ToolRegistry = {
|
|
|
|
[key: string]: ToolRegistryEntry;
|
|
|
|
};
|
|
|
|
|
|
|
|
const toolRegistry: ToolRegistry = {
|
|
|
|
split: { icon: <ContentCutIcon />, name: "Split PDF", component: SplitPdfPanel, view: "viewer" },
|
|
|
|
compress: { icon: <ZoomInMapIcon />, name: "Compress PDF", component: CompressPdfPanel, view: "viewer" },
|
|
|
|
merge: { icon: <AddToPhotosIcon />, name: "Merge PDFs", component: MergePdfPanel, view: "fileManager" },
|
|
|
|
};
|
|
|
|
|
|
|
|
const VIEW_OPTIONS = [
|
|
|
|
{
|
|
|
|
label: (
|
|
|
|
<Group gap={4}>
|
|
|
|
<VisibilityIcon fontSize="small" />
|
|
|
|
</Group>
|
|
|
|
),
|
|
|
|
value: "viewer",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: (
|
|
|
|
<Group gap={4}>
|
|
|
|
<EditNoteIcon fontSize="small" />
|
|
|
|
</Group>
|
|
|
|
),
|
|
|
|
value: "pageEditor",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
label: (
|
|
|
|
<Group gap={4}>
|
|
|
|
<InsertDriveFileIcon fontSize="small" />
|
|
|
|
</Group>
|
|
|
|
),
|
|
|
|
value: "fileManager",
|
|
|
|
},
|
|
|
|
];
|
|
|
|
|
2025-05-27 19:22:26 +01:00
|
|
|
// 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
|
|
|
|
};
|
|
|
|
|
2025-05-21 21:47:44 +01:00
|
|
|
export default function HomePage() {
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
2025-05-27 19:22:26 +01:00
|
|
|
const theme = useMantineTheme();
|
2025-05-21 21:47:44 +01:00
|
|
|
|
|
|
|
// Core app state
|
|
|
|
const [selectedToolKey, setSelectedToolKey] = useState<string>(searchParams.get("tool") || "split");
|
|
|
|
const [currentView, setCurrentView] = useState<string>(searchParams.get("view") || "viewer");
|
|
|
|
const [pdfFile, setPdfFile] = useState<any>(null);
|
|
|
|
const [files, setFiles] = useState<any[]>([]);
|
|
|
|
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
2025-05-27 19:22:26 +01:00
|
|
|
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
|
|
|
|
|
|
|
const toolParams = getToolParams(selectedToolKey, searchParams);
|
2025-05-21 21:47:44 +01:00
|
|
|
|
2025-05-27 19:22:26 +01:00
|
|
|
const updateParams = (newParams: any) =>
|
|
|
|
updateToolParams(selectedToolKey, searchParams, setSearchParams, newParams);
|
2025-05-21 21:47:44 +01:00
|
|
|
|
|
|
|
// Update URL when core state changes
|
|
|
|
useEffect(() => {
|
|
|
|
const params = new URLSearchParams(searchParams);
|
2025-05-27 19:22:26 +01:00
|
|
|
|
|
|
|
// 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]);
|
2025-05-21 21:47:44 +01:00
|
|
|
|
|
|
|
// Handle tool selection
|
|
|
|
const handleToolSelect = useCallback(
|
|
|
|
(id: string) => {
|
|
|
|
setSelectedToolKey(id);
|
|
|
|
if (toolRegistry[id]?.view) setCurrentView(toolRegistry[id].view);
|
|
|
|
},
|
|
|
|
[toolRegistry]
|
|
|
|
);
|
|
|
|
|
|
|
|
const selectedTool = toolRegistry[selectedToolKey];
|
|
|
|
|
|
|
|
// Tool component rendering
|
|
|
|
const renderTool = () => {
|
|
|
|
if (!selectedTool || !selectedTool.component) {
|
|
|
|
return <div>Tool not found</div>;
|
|
|
|
}
|
|
|
|
|
2025-05-27 19:22:26 +01:00
|
|
|
// Pass only the necessary props
|
2025-05-21 21:47:44 +01:00
|
|
|
return React.createElement(selectedTool.component, {
|
|
|
|
files,
|
|
|
|
setDownloadUrl,
|
2025-05-27 19:22:26 +01:00
|
|
|
params: toolParams,
|
|
|
|
updateParams,
|
2025-05-21 21:47:44 +01:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Group align="flex-start" gap={0} style={{ minHeight: "100vh" }}>
|
|
|
|
{/* Left: Tool Picker */}
|
2025-05-27 19:22:26 +01:00
|
|
|
{sidebarsVisible && (
|
|
|
|
<ToolPicker
|
|
|
|
selectedToolKey={selectedToolKey}
|
|
|
|
onSelect={handleToolSelect}
|
|
|
|
toolRegistry={toolRegistry}
|
|
|
|
/>
|
|
|
|
)}
|
2025-05-21 21:47:44 +01:00
|
|
|
|
|
|
|
{/* Middle: Main View (Viewer, Editor, Manager) */}
|
|
|
|
<Box
|
|
|
|
style={{
|
2025-05-27 19:22:26 +01:00
|
|
|
width: sidebarsVisible
|
|
|
|
? "calc(100vw - 220px - 380px)"
|
|
|
|
: "100vw",
|
|
|
|
marginLeft: sidebarsVisible ? 220 : 0,
|
|
|
|
marginRight: sidebarsVisible ? 380 : 0,
|
|
|
|
position: "relative", // <-- important for absolute overlay
|
2025-05-21 21:47:44 +01:00
|
|
|
height: "100vh",
|
2025-05-27 19:22:26 +01:00
|
|
|
display: "flex",
|
|
|
|
flexDirection: "column",
|
|
|
|
transition: "all 0.3s",
|
2025-05-21 21:47:44 +01:00
|
|
|
}}
|
|
|
|
>
|
2025-05-27 19:22:26 +01:00
|
|
|
{/* 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>
|
2025-05-21 21:47:44 +01:00
|
|
|
</Box>
|
|
|
|
{/* Right: Tool Interaction */}
|
2025-05-27 19:22:26 +01:00
|
|
|
{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)}
|
2025-05-21 21:47:44 +01:00
|
|
|
>
|
2025-05-27 19:22:26 +01:00
|
|
|
{sidebarsVisible ? "Hide Sidebars" : "Show Sidebars"}
|
|
|
|
</Button>
|
2025-05-21 21:47:44 +01:00
|
|
|
</Group>
|
|
|
|
);
|
|
|
|
}
|