mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-02 13:48:15 +02:00
Homepage refactor
This commit is contained in:
parent
7dd79aa2a1
commit
0c76bf2c8b
@ -6,17 +6,17 @@ const DeepLinks: React.FC = () => {
|
|||||||
const commonLinks = [
|
const commonLinks = [
|
||||||
{
|
{
|
||||||
name: "Split PDF Pages 1-5",
|
name: "Split PDF Pages 1-5",
|
||||||
url: "/?tool=split&splitMode=byPages&pages=1-5&view=viewer",
|
url: "/?t=split&mode=byPages&p=1-5&v=viewer",
|
||||||
description: "Split a PDF and extract pages 1-5"
|
description: "Split a PDF and extract pages 1-5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Compress PDF (High)",
|
name: "Compress PDF (High)",
|
||||||
url: "/?tool=compress&level=9&grayscale=true&view=viewer",
|
url: "/?t=compress&level=9&gray=true&v=viewer",
|
||||||
description: "Compress a PDF with high compression level"
|
description: "Compress a PDF with high compression level"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Merge PDFs",
|
name: "Merge PDFs",
|
||||||
url: "/?tool=merge&view=fileManager",
|
url: "/?t=merge&v=fileManager",
|
||||||
description: "Combine multiple PDF files into one"
|
description: "Combine multiple PDF files into one"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
74
frontend/src/components/ToolRenderer.tsx
Normal file
74
frontend/src/components/ToolRenderer.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FileWithUrl } from "../types/file";
|
||||||
|
|
||||||
|
interface ToolRendererProps {
|
||||||
|
selectedToolKey: string;
|
||||||
|
selectedTool: any;
|
||||||
|
pdfFile: any;
|
||||||
|
files: FileWithUrl[];
|
||||||
|
downloadUrl: string | null;
|
||||||
|
setDownloadUrl: (url: string | null) => void;
|
||||||
|
toolParams: any;
|
||||||
|
updateParams: (params: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolRenderer: React.FC<ToolRendererProps> = ({
|
||||||
|
selectedToolKey,
|
||||||
|
selectedTool,
|
||||||
|
pdfFile,
|
||||||
|
files,
|
||||||
|
downloadUrl,
|
||||||
|
setDownloadUrl,
|
||||||
|
toolParams,
|
||||||
|
updateParams,
|
||||||
|
}) => {
|
||||||
|
if (!selectedTool || !selectedTool.component) {
|
||||||
|
return <div>Tool not found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolComponent = selectedTool.component;
|
||||||
|
|
||||||
|
// Pass tool-specific props
|
||||||
|
switch (selectedToolKey) {
|
||||||
|
case "split":
|
||||||
|
return (
|
||||||
|
<ToolComponent
|
||||||
|
file={pdfFile}
|
||||||
|
downloadUrl={downloadUrl}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
params={toolParams}
|
||||||
|
updateParams={updateParams}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "compress":
|
||||||
|
return (
|
||||||
|
<ToolComponent
|
||||||
|
files={files}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
setLoading={(loading: boolean) => {}} // TODO: Add loading state
|
||||||
|
params={toolParams}
|
||||||
|
updateParams={updateParams}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "merge":
|
||||||
|
return (
|
||||||
|
<ToolComponent
|
||||||
|
files={files}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
params={toolParams}
|
||||||
|
updateParams={updateParams}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<ToolComponent
|
||||||
|
files={files}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
params={toolParams}
|
||||||
|
updateParams={updateParams}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolRenderer;
|
106
frontend/src/components/TopControls.tsx
Normal file
106
frontend/src/components/TopControls.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, SegmentedControl } from "@mantine/core";
|
||||||
|
import { useMantineColorScheme } from "@mantine/core";
|
||||||
|
import LanguageSelector from "./LanguageSelector";
|
||||||
|
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||||
|
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
import EditNoteIcon from "@mui/icons-material/EditNote";
|
||||||
|
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface TopControlsProps {
|
||||||
|
currentView: string;
|
||||||
|
setCurrentView: (view: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopControls: React.FC<TopControlsProps> = ({
|
||||||
|
currentView,
|
||||||
|
setCurrentView,
|
||||||
|
}) => {
|
||||||
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
top: 0,
|
||||||
|
zIndex: 30,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 16,
|
||||||
|
top: "50%",
|
||||||
|
transform: "translateY(-50%)",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
display: "flex",
|
||||||
|
gap: 12,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={toggleColorScheme}
|
||||||
|
variant="subtle"
|
||||||
|
size="md"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
{colorScheme === "dark" ? <LightModeIcon /> : <DarkModeIcon />}
|
||||||
|
</Button>
|
||||||
|
<LanguageSelector />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
height: "100%",
|
||||||
|
pointerEvents: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SegmentedControl
|
||||||
|
data={VIEW_OPTIONS}
|
||||||
|
value={currentView}
|
||||||
|
onChange={setCurrentView}
|
||||||
|
color="blue"
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TopControls;
|
130
frontend/src/hooks/useToolParams.ts
Normal file
130
frontend/src/hooks/useToolParams.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// Tool parameter definitions (shortened URLs)
|
||||||
|
const TOOL_PARAMS = {
|
||||||
|
split: [
|
||||||
|
"mode", "p", "hd", "vd", "m",
|
||||||
|
"type", "val", "level", "meta", "dupes"
|
||||||
|
],
|
||||||
|
compress: [
|
||||||
|
"level", "gray", "rmeta", "size", "agg"
|
||||||
|
],
|
||||||
|
merge: [
|
||||||
|
"order", "rdupes"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract params for a specific tool from URL
|
||||||
|
function getToolParams(toolKey: string, searchParams: URLSearchParams) {
|
||||||
|
switch (toolKey) {
|
||||||
|
case "split":
|
||||||
|
return {
|
||||||
|
mode: searchParams.get("mode") || "byPages",
|
||||||
|
pages: searchParams.get("p") || "",
|
||||||
|
hDiv: searchParams.get("hd") || "",
|
||||||
|
vDiv: searchParams.get("vd") || "",
|
||||||
|
merge: searchParams.get("m") === "true",
|
||||||
|
splitType: searchParams.get("type") || "size",
|
||||||
|
splitValue: searchParams.get("val") || "",
|
||||||
|
bookmarkLevel: searchParams.get("level") || "0",
|
||||||
|
includeMetadata: searchParams.get("meta") === "true",
|
||||||
|
allowDuplicates: searchParams.get("dupes") === "true",
|
||||||
|
};
|
||||||
|
case "compress":
|
||||||
|
return {
|
||||||
|
compressionLevel: parseInt(searchParams.get("level") || "5"),
|
||||||
|
grayscale: searchParams.get("gray") === "true",
|
||||||
|
removeMetadata: searchParams.get("rmeta") === "true",
|
||||||
|
expectedSize: searchParams.get("size") || "",
|
||||||
|
aggressive: searchParams.get("agg") === "true",
|
||||||
|
};
|
||||||
|
case "merge":
|
||||||
|
return {
|
||||||
|
order: searchParams.get("order") || "default",
|
||||||
|
removeDuplicates: searchParams.get("rdupes") === "true",
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tool-specific params in URL
|
||||||
|
function updateToolParams(toolKey: string, searchParams: URLSearchParams, setSearchParams: any, newParams: any) {
|
||||||
|
const params = new URLSearchParams(searchParams);
|
||||||
|
|
||||||
|
// Clear tool-specific params
|
||||||
|
if (toolKey === "split") {
|
||||||
|
["mode", "p", "hd", "vd", "m", "type", "val", "level", "meta", "dupes"].forEach((k) => params.delete(k));
|
||||||
|
// Set new split params
|
||||||
|
const merged = { ...getToolParams("split", searchParams), ...newParams };
|
||||||
|
params.set("mode", merged.mode);
|
||||||
|
if (merged.mode === "byPages") params.set("p", merged.pages);
|
||||||
|
else if (merged.mode === "bySections") {
|
||||||
|
params.set("hd", merged.hDiv);
|
||||||
|
params.set("vd", merged.vDiv);
|
||||||
|
params.set("m", String(merged.merge));
|
||||||
|
} else if (merged.mode === "bySizeOrCount") {
|
||||||
|
params.set("type", merged.splitType);
|
||||||
|
params.set("val", merged.splitValue);
|
||||||
|
} else if (merged.mode === "byChapters") {
|
||||||
|
params.set("level", merged.bookmarkLevel);
|
||||||
|
params.set("meta", String(merged.includeMetadata));
|
||||||
|
params.set("dupes", String(merged.allowDuplicates));
|
||||||
|
}
|
||||||
|
} else if (toolKey === "compress") {
|
||||||
|
["level", "gray", "rmeta", "size", "agg"].forEach((k) => params.delete(k));
|
||||||
|
const merged = { ...getToolParams("compress", searchParams), ...newParams };
|
||||||
|
params.set("level", String(merged.compressionLevel));
|
||||||
|
params.set("gray", String(merged.grayscale));
|
||||||
|
params.set("rmeta", String(merged.removeMetadata));
|
||||||
|
if (merged.expectedSize) params.set("size", merged.expectedSize);
|
||||||
|
params.set("agg", String(merged.aggressive));
|
||||||
|
} else if (toolKey === "merge") {
|
||||||
|
["order", "rdupes"].forEach((k) => params.delete(k));
|
||||||
|
const merged = { ...getToolParams("merge", searchParams), ...newParams };
|
||||||
|
params.set("order", merged.order);
|
||||||
|
params.set("rdupes", String(merged.removeDuplicates));
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(params, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToolParams(selectedToolKey: string, currentView: string) {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
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 'v'
|
||||||
|
const entries = Array.from(params.entries()).filter(([key]) => key !== "v");
|
||||||
|
|
||||||
|
// Rebuild params with 'v' first
|
||||||
|
const newParams = new URLSearchParams();
|
||||||
|
newParams.set("v", currentView);
|
||||||
|
newParams.set("t", selectedToolKey);
|
||||||
|
entries.forEach(([key, value]) => {
|
||||||
|
if (key !== "t") newParams.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
setSearchParams(newParams, { replace: true });
|
||||||
|
}, [selectedToolKey, currentView, setSearchParams, searchParams]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
toolParams,
|
||||||
|
updateParams,
|
||||||
|
};
|
||||||
|
}
|
@ -1,13 +1,11 @@
|
|||||||
import React, { useState, useCallback, useEffect } from "react";
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { useToolParams } from "../hooks/useToolParams";
|
||||||
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
||||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||||
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
import { Group, Paper, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
||||||
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 ToolPicker from "../components/ToolPicker";
|
||||||
import FileManager from "../components/FileManager";
|
import FileManager from "../components/FileManager";
|
||||||
@ -16,9 +14,9 @@ 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 LanguageSelector from "../components/LanguageSelector";
|
import TopControls from "../components/TopControls";
|
||||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
import ToolRenderer from "../components/ToolRenderer";
|
||||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
import styles from "../styles/HomePage.module.css";
|
||||||
|
|
||||||
type ToolRegistryEntry = {
|
type ToolRegistryEntry = {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@ -38,134 +36,25 @@ const baseToolRegistry = {
|
|||||||
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "fileManager" },
|
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") || "",
|
|
||||||
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 {
|
|
||||||
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") {
|
|
||||||
["compressionLevel", "grayscale", "removeMetadata", "expectedSize", "aggressive"].forEach((k) => params.delete(k));
|
|
||||||
const merged = { ...getToolParams("compress", searchParams), ...newParams };
|
|
||||||
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: [
|
|
||||||
"compressionLevel", "grayscale", "removeMetadata", "expectedSize", "aggressive"
|
|
||||||
],
|
|
||||||
merge: [
|
|
||||||
"mergeOrder", "removeDuplicates"
|
|
||||||
]
|
|
||||||
// Add more tools as needed
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
// Core app state
|
||||||
|
const [selectedToolKey, setSelectedToolKey] = useState<string>(searchParams.get("t") || "split");
|
||||||
|
const [currentView, setCurrentView] = useState<string>(searchParams.get("v") || "viewer");
|
||||||
|
const [pdfFile, setPdfFile] = useState<any>(null);
|
||||||
|
const [files, setFiles] = useState<any[]>([]);
|
||||||
|
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||||
|
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||||
|
|
||||||
|
// URL parameter management
|
||||||
|
const { toolParams, updateParams } = useToolParams(selectedToolKey, currentView);
|
||||||
|
|
||||||
// Create translated tool registry
|
// Create translated tool registry
|
||||||
const toolRegistry: ToolRegistry = {
|
const toolRegistry: ToolRegistry = {
|
||||||
split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") },
|
split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") },
|
||||||
@ -173,43 +62,6 @@ export default function HomePage() {
|
|||||||
merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") },
|
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
|
// Handle tool selection
|
||||||
const handleToolSelect = useCallback(
|
const handleToolSelect = useCallback(
|
||||||
@ -222,73 +74,18 @@ export default function HomePage() {
|
|||||||
|
|
||||||
const selectedTool = toolRegistry[selectedToolKey];
|
const selectedTool = toolRegistry[selectedToolKey];
|
||||||
|
|
||||||
// Tool component rendering
|
|
||||||
const renderTool = () => {
|
|
||||||
if (!selectedTool || !selectedTool.component) {
|
|
||||||
return <div>Tool not found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
return (
|
||||||
<Group
|
<Group
|
||||||
align="flex-start"
|
align="flex-start"
|
||||||
gap={0}
|
gap={0}
|
||||||
style={{
|
className={styles.container}
|
||||||
minHeight: "100vh",
|
|
||||||
width: "100vw",
|
|
||||||
overflow: "hidden",
|
|
||||||
flexWrap: "nowrap",
|
|
||||||
display: "flex",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* Left: Tool Picker */}
|
{/* Left: Tool Picker */}
|
||||||
{sidebarsVisible && (
|
{sidebarsVisible && (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
className={`${styles.leftSidebar} ${
|
||||||
minWidth: 180,
|
colorScheme === "dark" ? styles.leftSidebarDark : styles.leftSidebarLight
|
||||||
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
|
<ToolPicker
|
||||||
selectedToolKey={selectedToolKey}
|
selectedToolKey={selectedToolKey}
|
||||||
@ -300,86 +97,23 @@ export default function HomePage() {
|
|||||||
|
|
||||||
{/* Middle: Main View */}
|
{/* Middle: Main View */}
|
||||||
<Box
|
<Box
|
||||||
style={{
|
className={`${styles.mainContent} ${
|
||||||
flex: 1,
|
colorScheme === "dark" ? styles.mainContentDark : styles.mainContentLight
|
||||||
height: "100vh",
|
}`}
|
||||||
minWidth: "20rem",
|
|
||||||
position: "relative",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
transition: "all 0.3s",
|
|
||||||
background: colorScheme === "dark" ? theme.colors.dark[6] : "#f8f9fa",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{/* Overlayed View Switcher + Theme Toggle */}
|
{/* Top Controls */}
|
||||||
<div
|
<TopControls
|
||||||
style={{
|
currentView={currentView}
|
||||||
position: "absolute",
|
setCurrentView={setCurrentView}
|
||||||
left: 0,
|
|
||||||
width: "100%",
|
|
||||||
top: 0,
|
|
||||||
zIndex: 30,
|
|
||||||
pointerEvents: "none",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
left: 16,
|
|
||||||
top: "50%",
|
|
||||||
transform: "translateY(-50%)",
|
|
||||||
pointerEvents: "auto",
|
|
||||||
display: "flex",
|
|
||||||
gap: 12,
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
onClick={toggleColorScheme}
|
|
||||||
variant="subtle"
|
|
||||||
size="md"
|
|
||||||
aria-label="Toggle theme"
|
|
||||||
>
|
|
||||||
{colorScheme === "dark" ? <LightModeIcon /> : <DarkModeIcon />}
|
|
||||||
</Button>
|
|
||||||
<LanguageSelector />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
height: "100%",
|
|
||||||
pointerEvents: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SegmentedControl
|
|
||||||
data={VIEW_OPTIONS}
|
|
||||||
value={currentView}
|
|
||||||
onChange={setCurrentView}
|
|
||||||
color="blue"
|
|
||||||
radius="xl"
|
|
||||||
size="md"
|
|
||||||
fullWidth
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<Paper
|
<Paper
|
||||||
radius="0 0 xl xl"
|
radius="0 0 xl xl"
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
p={0}
|
p={0}
|
||||||
style={{
|
className={styles.mainPaper}
|
||||||
flex: 1,
|
|
||||||
minHeight: 0,
|
|
||||||
marginTop: 0,
|
|
||||||
boxSizing: "border-box",
|
|
||||||
overflow: "hidden",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box style={{ flex: 1, minHeight: 0 }}>
|
<Box className={styles.mainPaperInner}>
|
||||||
{(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
|
{(currentView === "viewer" || currentView === "pageEditor") && !pdfFile ? (
|
||||||
<FileManager
|
<FileManager
|
||||||
files={files}
|
files={files}
|
||||||
@ -416,21 +150,20 @@ export default function HomePage() {
|
|||||||
{/* Right: Tool Interaction */}
|
{/* Right: Tool Interaction */}
|
||||||
{sidebarsVisible && (
|
{sidebarsVisible && (
|
||||||
<Box
|
<Box
|
||||||
style={{
|
className={`${styles.rightSidebar} ${
|
||||||
minWidth: 260,
|
colorScheme === "dark" ? styles.rightSidebarDark : styles.rightSidebarLight
|
||||||
maxWidth: 400,
|
}`}
|
||||||
width: "22vw",
|
|
||||||
height: "100vh",
|
|
||||||
borderLeft: `1px solid ${colorScheme === "dark" ? theme.colors.dark[4] : "#e9ecef"}`,
|
|
||||||
background: colorScheme === "dark" ? theme.colors.dark[7] : "#fff",
|
|
||||||
padding: 24,
|
|
||||||
gap: 16,
|
|
||||||
zIndex: 100,
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{selectedTool && selectedTool.component && renderTool()}
|
<ToolRenderer
|
||||||
|
selectedToolKey={selectedToolKey}
|
||||||
|
selectedTool={selectedTool}
|
||||||
|
pdfFile={pdfFile}
|
||||||
|
files={files}
|
||||||
|
downloadUrl={downloadUrl}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
toolParams={toolParams}
|
||||||
|
updateParams={updateParams}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -439,7 +172,7 @@ export default function HomePage() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
size="xs"
|
size="xs"
|
||||||
style={{ position: "fixed", top: 16, right: 16, zIndex: 200 }}
|
className={styles.sidebarToggle}
|
||||||
onClick={() => setSidebarsVisible((v) => !v)}
|
onClick={() => setSidebarsVisible((v) => !v)}
|
||||||
>
|
>
|
||||||
{t("sidebar.toggle", sidebarsVisible ? "Hide Sidebars" : "Show Sidebars")}
|
{t("sidebar.toggle", sidebarsVisible ? "Hide Sidebars" : "Show Sidebars")}
|
||||||
|
95
frontend/src/styles/HomePage.module.css
Normal file
95
frontend/src/styles/HomePage.module.css
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
/* Main container */
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left sidebar */
|
||||||
|
.leftSidebar {
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 240px;
|
||||||
|
width: 16vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 101;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebarLight {
|
||||||
|
border-right: 1px solid #e9ecef;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftSidebarDark {
|
||||||
|
border-right: 1px solid var(--mantine-color-dark-4);
|
||||||
|
background: var(--mantine-color-dark-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main content area */
|
||||||
|
.mainContent {
|
||||||
|
flex: 1;
|
||||||
|
height: 100vh;
|
||||||
|
min-width: 20rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContentLight {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContentDark {
|
||||||
|
background: var(--mantine-color-dark-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main paper container */
|
||||||
|
.mainPaper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainPaperInner {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right sidebar */
|
||||||
|
.rightSidebar {
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 22vw;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 24px;
|
||||||
|
gap: 16px;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightSidebarLight {
|
||||||
|
border-left: 1px solid #e9ecef;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightSidebarDark {
|
||||||
|
border-left: 1px solid var(--mantine-color-dark-4);
|
||||||
|
background: var(--mantine-color-dark-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar toggle button */
|
||||||
|
.sidebarToggle {
|
||||||
|
position: fixed;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 200;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user