Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

450 lines
14 KiB
TypeScript
Raw Normal View History

import React, { useState, useCallback, useEffect } from "react";
2025-05-29 17:26:32 +01:00
import { useTranslation } from 'react-i18next';
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";
import { Group, SegmentedControl, Paper, Center, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core";
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-29 17:26:32 +01:00
import LanguageSelector from "../components/LanguageSelector";
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
type ToolRegistryEntry = {
icon: React.ReactNode;
name: string;
component: React.ComponentType<any>;
view: string;
};
type ToolRegistry = {
[key: string]: ToolRegistryEntry;
};
2025-05-29 17:26:32 +01:00
// Base tool registry without translations
const baseToolRegistry = {
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "viewer" },
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "viewer" },
merge: { icon: <AddToPhotosIcon />, 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",
},
];
// 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") || "",
2025-06-04 23:02:53 +01:00
hDiv: searchParams.get("hDiv") || "",
vDiv: searchParams.get("vDiv") || "",
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 {
2025-06-04 23:02:53 +01:00
compressionLevel: parseInt(searchParams.get("compressionLevel") || "5"),
grayscale: searchParams.get("grayscale") === "true",
removeMetadata: searchParams.get("removeMetadata") === "true",
expectedSize: searchParams.get("expectedSize") || "",
aggressive: searchParams.get("aggressive") === "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") {
2025-06-04 23:02:53 +01:00
["compressionLevel", "grayscale", "removeMetadata", "expectedSize", "aggressive"].forEach((k) => params.delete(k));
const merged = { ...getToolParams("compress", searchParams), ...newParams };
2025-06-04 23:02:53 +01:00
params.set("compressionLevel", String(merged.compressionLevel));
params.set("grayscale", String(merged.grayscale));
params.set("removeMetadata", String(merged.removeMetadata));
if (merged.expectedSize) params.set("expectedSize", merged.expectedSize);
params.set("aggressive", String(merged.aggressive));
} 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: [
2025-06-04 23:02:53 +01:00
"compressionLevel", "grayscale", "removeMetadata", "expectedSize", "aggressive"
],
merge: [
"mergeOrder", "removeDuplicates"
]
// Add more tools as needed
};
export default function HomePage() {
2025-05-29 17:26:32 +01:00
const { t } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const theme = useMantineTheme();
2025-05-28 22:18:19 +01:00
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
2025-05-29 17:26:32 +01:00
// Create translated tool registry
const toolRegistry: ToolRegistry = {
split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") },
compress: { ...baseToolRegistry.compress, name: t("home.compressPdfs.title", "Compress PDF") },
merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") },
};
// 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);
const [sidebarsVisible, setSidebarsVisible] = useState(true);
const toolParams = getToolParams(selectedToolKey, searchParams);
const updateParams = (newParams: any) =>
updateToolParams(selectedToolKey, searchParams, setSearchParams, newParams);
// Update URL when core state changes
useEffect(() => {
const params = new URLSearchParams(searchParams);
// Remove all tool-specific params except for the current tool
Object.entries(TOOL_PARAMS).forEach(([tool, keys]) => {
if (tool !== selectedToolKey) {
keys.forEach((k) => params.delete(k));
}
});
// Collect all params except 'view'
const entries = Array.from(params.entries()).filter(([key]) => key !== "view");
// Rebuild params with 'view' first
const newParams = new URLSearchParams();
newParams.set("view", currentView);
newParams.set("tool", selectedToolKey);
entries.forEach(([key, value]) => {
if (key !== "tool") newParams.set(key, value);
});
setSearchParams(newParams, { replace: true });
}, [selectedToolKey, currentView, setSearchParams, searchParams]);
// Handle tool selection
const handleToolSelect = useCallback(
(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-06-04 23:02:53 +01:00
// Pass tool-specific props
switch (selectedToolKey) {
case "split":
return React.createElement(selectedTool.component, {
file: pdfFile,
downloadUrl,
setDownloadUrl,
params: toolParams,
updateParams,
});
case "compress":
return React.createElement(selectedTool.component, {
files,
setDownloadUrl,
setLoading: (loading: boolean) => {}, // TODO: Add loading state
params: toolParams,
updateParams,
});
case "merge":
return React.createElement(selectedTool.component, {
files,
setDownloadUrl,
params: toolParams,
updateParams,
});
default:
return React.createElement(selectedTool.component, {
files,
setDownloadUrl,
params: toolParams,
updateParams,
});
}
};
return (
2025-05-28 21:43:02 +01:00
<Group
align="flex-start"
gap={0}
style={{
minHeight: "100vh",
width: "100vw",
overflow: "hidden",
flexWrap: "nowrap",
display: "flex",
}}
>
{/* Left: Tool Picker */}
{sidebarsVisible && (
2025-05-28 21:43:02 +01:00
<Box
style={{
minWidth: 180,
maxWidth: 240,
width: "16vw",
height: "100vh",
borderRight: `1px solid ${colorScheme === "dark" ? theme.colors.dark[4] : "#e9ecef"}`,
background: colorScheme === "dark" ? theme.colors.dark[7] : "#fff",
zIndex: 101,
display: "flex",
flexDirection: "column",
}}
>
<ToolPicker
selectedToolKey={selectedToolKey}
onSelect={handleToolSelect}
toolRegistry={toolRegistry}
/>
</Box>
)}
2025-05-28 21:43:02 +01:00
{/* Middle: Main View */}
<Box
style={{
2025-05-28 21:43:02 +01:00
flex: 1,
height: "100vh",
2025-05-28 22:18:19 +01:00
minWidth: "20rem",
2025-05-28 21:43:02 +01:00
position: "relative",
display: "flex",
flexDirection: "column",
transition: "all 0.3s",
2025-05-28 21:43:02 +01:00
background: colorScheme === "dark" ? theme.colors.dark[6] : "#f8f9fa",
}}
>
2025-05-28 22:18:19 +01:00
{/* Overlayed View Switcher + Theme Toggle */}
<div
style={{
position: "absolute",
left: 0,
width: "100%",
2025-05-28 22:18:19 +01:00
top: 0,
zIndex: 30,
2025-05-28 21:43:02 +01:00
pointerEvents: "none",
}}
>
2025-05-28 22:18:19 +01:00
<div
style={{
position: "absolute",
left: 16,
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "auto",
2025-05-29 17:26:32 +01:00
display: "flex",
gap: 12,
alignItems: "center",
2025-05-28 22:18:19 +01:00
}}
>
<Button
onClick={toggleColorScheme}
variant="subtle"
size="md"
aria-label="Toggle theme"
>
{colorScheme === "dark" ? <LightModeIcon /> : <DarkModeIcon />}
</Button>
2025-05-29 17:26:32 +01:00
<LanguageSelector />
2025-05-28 22:18:19 +01:00
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
pointerEvents: "auto",
}}
>
2025-05-28 21:43:02 +01:00
<SegmentedControl
data={VIEW_OPTIONS}
value={currentView}
onChange={setCurrentView}
color="blue"
radius="xl"
size="md"
fullWidth
/>
</div>
</div>
2025-05-28 21:43:02 +01:00
{/* Main content area */}
<Paper
radius="0 0 xl xl"
shadow="sm"
p={0}
style={{
flex: 1,
minHeight: 0,
marginTop: 0,
boxSizing: "border-box",
overflow: "hidden",
display: "flex",
flexDirection: "column",
}}
>
<Box style={{ flex: 1, minHeight: 0 }}>
{(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
<FileManager
files={files}
setFiles={setFiles}
setPdfFile={setPdfFile}
setCurrentView={setCurrentView}
/>
) : currentView === "viewer" ? (
<Viewer
pdfFile={pdfFile}
setPdfFile={setPdfFile}
sidebarsVisible={sidebarsVisible}
setSidebarsVisible={setSidebarsVisible}
/>
) : currentView === "pageEditor" ? (
<PageEditor
file={pdfFile}
setFile={setPdfFile}
downloadUrl={downloadUrl}
setDownloadUrl={setDownloadUrl}
/>
) : (
<FileManager
files={files}
setFiles={setFiles}
setPdfFile={setPdfFile}
setCurrentView={setCurrentView}
/>
)}
</Box>
</Paper>
</Box>
2025-05-28 21:43:02 +01:00
{/* Right: Tool Interaction */}
{sidebarsVisible && (
<Box
style={{
2025-05-28 21:43:02 +01:00
minWidth: 260,
maxWidth: 400,
width: "22vw",
height: "100vh",
2025-05-28 21:43:02 +01:00
borderLeft: `1px solid ${colorScheme === "dark" ? theme.colors.dark[4] : "#e9ecef"}`,
background: colorScheme === "dark" ? theme.colors.dark[7] : "#fff",
padding: 24,
gap: 16,
zIndex: 100,
2025-05-28 21:43:02 +01:00
display: "flex",
flexDirection: "column",
}}
>
2025-05-28 21:43:02 +01:00
{selectedTool && selectedTool.component && renderTool()}
</Box>
)}
2025-05-28 21:43:02 +01:00
{/* Sidebar toggle button */}
<Button
variant="light"
color="blue"
size="xs"
2025-05-28 21:43:02 +01:00
style={{ position: "fixed", top: 16, right: 16, zIndex: 200 }}
onClick={() => setSidebarsVisible((v) => !v)}
>
2025-05-29 17:26:32 +01:00
{t("sidebar.toggle", sidebarsVisible ? "Hide Sidebars" : "Show Sidebars")}
</Button>
</Group>
);
}