mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-15 03:55:02 +00:00
Mantine overhaul
This commit is contained in:
parent
d669964975
commit
f789533c83
911
frontend/package-lock.json
generated
911
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
|
"@mantine/core": "^8.0.1",
|
||||||
|
"@mantine/hooks": "^8.0.1",
|
||||||
"@mui/icons-material": "^7.1.0",
|
"@mui/icons-material": "^7.1.0",
|
||||||
"@mui/material": "^7.1.0",
|
"@mui/material": "^7.1.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
@ -42,5 +44,11 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"postcss-cli": "^11.0.1",
|
||||||
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
|
"postcss-simple-vars": "^7.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,6 @@ module.exports = {
|
|||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,31 @@
|
|||||||
import React from "react";
|
import React from 'react';
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
|
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
|
||||||
|
import './index.css';
|
||||||
|
import HomePage from './pages/HomePage';
|
||||||
|
import SplitPdfPanel from './tools/Split';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
import HomePage from "./pages/HomePage";
|
export default function App() {
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Routes>
|
||||||
<Routes>
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/split" element={<SplitPdfPanel />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ColorSchemeScript />
|
||||||
|
<MantineProvider withGlobalStyles withNormalizeCSS>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</MantineProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
reportWebVitals();
|
||||||
|
37
frontend/src/components/FileManager.js
Normal file
37
frontend/src/components/FileManager.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function FileManager({ files, setFiles, allowMultiple = true }) {
|
||||||
|
const handleFileUpload = (e) => {
|
||||||
|
const uploadedFiles = Array.from(e.target.files);
|
||||||
|
setFiles((prevFiles) => (allowMultiple ? [...prevFiles, ...uploadedFiles] : uploadedFiles));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = (index) => {
|
||||||
|
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 w-full max-w-3xl">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="application/pdf"
|
||||||
|
multiple={allowMultiple}
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
className="block"
|
||||||
|
/>
|
||||||
|
<ul className="list-disc pl-5 text-sm">
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<li key={index} className="flex justify-between items-center">
|
||||||
|
{file.name}
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveFile(index)}
|
||||||
|
className="text-red-600 hover:underline text-xs"
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
9
frontend/src/components/PageEditor.js
Normal file
9
frontend/src/components/PageEditor.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function PageEditor({ pdfFile }) {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<p className="text-gray-500">Page Editor is under construction.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
27
frontend/src/components/Viewer.js
Normal file
27
frontend/src/components/Viewer.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function Viewer({ pdfFile, setPdfFile }) {
|
||||||
|
return pdfFile ? (
|
||||||
|
<iframe
|
||||||
|
src={pdfFile.url}
|
||||||
|
title="PDF Viewer"
|
||||||
|
className="w-full h-full border-none"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<label className="cursor-pointer text-blue-600 underline">
|
||||||
|
Click to upload a PDF
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="application/pdf"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file && file.type === "application/pdf") {
|
||||||
|
const fileUrl = URL.createObjectURL(file);
|
||||||
|
setPdfFile({ file, url: fileUrl });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
@ -1,17 +1,22 @@
|
|||||||
|
import '@mantine/core/styles.css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { ColorSchemeScript, MantineProvider, mantineHtmlProps } from '@mantine/core';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import './index.css';
|
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'));
|
const root = ReactDOM.createRoot(document.getElementById('root')); // Finds the root DOM element
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ColorSchemeScript />
|
||||||
|
<MantineProvider withGlobalStyles withNormalizeCSS>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</MantineProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
reportWebVitals();
|
||||||
|
362
frontend/src/output.css
Normal file
362
frontend/src/output.css
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
*, ::before, ::after {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style:
|
||||||
|
}
|
||||||
|
::backdrop {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
--tw-contain-size: ;
|
||||||
|
--tw-contain-layout: ;
|
||||||
|
--tw-contain-paint: ;
|
||||||
|
--tw-contain-style:
|
||||||
|
}
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: 0.75rem
|
||||||
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1rem
|
||||||
|
}
|
||||||
|
.mr-2 {
|
||||||
|
margin-right: 0.5rem
|
||||||
|
}
|
||||||
|
.mt-2 {
|
||||||
|
margin-top: 0.5rem
|
||||||
|
}
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: 1rem
|
||||||
|
}
|
||||||
|
.block {
|
||||||
|
display: block
|
||||||
|
}
|
||||||
|
.flex {
|
||||||
|
display: flex
|
||||||
|
}
|
||||||
|
.hidden {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
.h-6 {
|
||||||
|
height: 1.5rem
|
||||||
|
}
|
||||||
|
.h-full {
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
.h-screen {
|
||||||
|
height: 100vh
|
||||||
|
}
|
||||||
|
.w-6 {
|
||||||
|
width: 1.5rem
|
||||||
|
}
|
||||||
|
.w-64 {
|
||||||
|
width: 16rem
|
||||||
|
}
|
||||||
|
.w-72 {
|
||||||
|
width: 18rem
|
||||||
|
}
|
||||||
|
.w-full {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
.max-w-3xl {
|
||||||
|
max-width: 48rem
|
||||||
|
}
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1 1 0%
|
||||||
|
}
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer
|
||||||
|
}
|
||||||
|
.list-disc {
|
||||||
|
list-style-type: disc
|
||||||
|
}
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column
|
||||||
|
}
|
||||||
|
.items-center {
|
||||||
|
align-items: center
|
||||||
|
}
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center
|
||||||
|
}
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between
|
||||||
|
}
|
||||||
|
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)))
|
||||||
|
}
|
||||||
|
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-x-reverse: 0;
|
||||||
|
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
|
||||||
|
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)))
|
||||||
|
}
|
||||||
|
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse))
|
||||||
|
}
|
||||||
|
.space-y-3 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse))
|
||||||
|
}
|
||||||
|
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
margin-bottom: calc(1rem * var(--tw-space-y-reverse))
|
||||||
|
}
|
||||||
|
.overflow-hidden {
|
||||||
|
overflow: hidden
|
||||||
|
}
|
||||||
|
.overflow-y-auto {
|
||||||
|
overflow-y: auto
|
||||||
|
}
|
||||||
|
.rounded {
|
||||||
|
border-radius: 0.25rem
|
||||||
|
}
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem
|
||||||
|
}
|
||||||
|
.border {
|
||||||
|
border-width: 1px
|
||||||
|
}
|
||||||
|
.border-b {
|
||||||
|
border-bottom-width: 1px
|
||||||
|
}
|
||||||
|
.border-l {
|
||||||
|
border-left-width: 1px
|
||||||
|
}
|
||||||
|
.border-r {
|
||||||
|
border-right-width: 1px
|
||||||
|
}
|
||||||
|
.border-none {
|
||||||
|
border-style: none
|
||||||
|
}
|
||||||
|
.bg-blue-600 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1))
|
||||||
|
}
|
||||||
|
.bg-gray-100 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1))
|
||||||
|
}
|
||||||
|
.bg-gray-50 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1))
|
||||||
|
}
|
||||||
|
.bg-green-600 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1))
|
||||||
|
}
|
||||||
|
.bg-white {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1))
|
||||||
|
}
|
||||||
|
.p-2 {
|
||||||
|
padding: 0.5rem
|
||||||
|
}
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem
|
||||||
|
}
|
||||||
|
.px-2 {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem
|
||||||
|
}
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem
|
||||||
|
}
|
||||||
|
.py-1 {
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
padding-bottom: 0.25rem
|
||||||
|
}
|
||||||
|
.py-2 {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem
|
||||||
|
}
|
||||||
|
.pl-5 {
|
||||||
|
padding-left: 1.25rem
|
||||||
|
}
|
||||||
|
.text-left {
|
||||||
|
text-align: left
|
||||||
|
}
|
||||||
|
.text-center {
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
.text-lg {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.75rem
|
||||||
|
}
|
||||||
|
.text-sm {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem
|
||||||
|
}
|
||||||
|
.text-xl {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.75rem
|
||||||
|
}
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem
|
||||||
|
}
|
||||||
|
.font-medium {
|
||||||
|
font-weight: 500
|
||||||
|
}
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600
|
||||||
|
}
|
||||||
|
.leading-none {
|
||||||
|
line-height: 1
|
||||||
|
}
|
||||||
|
.text-blue-600 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(37 99 235 / var(--tw-text-opacity, 1))
|
||||||
|
}
|
||||||
|
.text-gray-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(107 114 128 / var(--tw-text-opacity, 1))
|
||||||
|
}
|
||||||
|
.text-gray-600 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(75 85 99 / var(--tw-text-opacity, 1))
|
||||||
|
}
|
||||||
|
.text-red-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(239 68 68 / var(--tw-text-opacity, 1))
|
||||||
|
}
|
||||||
|
.text-red-600 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(220 38 38 / var(--tw-text-opacity, 1))
|
||||||
|
}
|
||||||
|
.text-white {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1))
|
||||||
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline
|
||||||
|
}
|
||||||
|
.shadow {
|
||||||
|
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||||
|
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)
|
||||||
|
}
|
||||||
|
.shadow-sm {
|
||||||
|
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||||
|
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)
|
||||||
|
}
|
||||||
|
.grayscale {
|
||||||
|
--tw-grayscale: grayscale(100%);
|
||||||
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)
|
||||||
|
}
|
||||||
|
.hover\:bg-blue-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1))
|
||||||
|
}
|
||||||
|
.hover\:bg-gray-200:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(229 231 235 / var(--tw-bg-opacity, 1))
|
||||||
|
}
|
||||||
|
.hover\:bg-green-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(21 128 61 / var(--tw-bg-opacity, 1))
|
||||||
|
}
|
||||||
|
.hover\:underline:hover {
|
||||||
|
text-decoration-line: underline
|
||||||
|
}
|
@ -1,230 +1,180 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import ConstructionIcon from '@mui/icons-material/Construction';
|
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 RotateRightIcon from '@mui/icons-material/RotateRight';
|
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
|
||||||
import CropIcon from '@mui/icons-material/Crop';
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
import EditNoteIcon from "@mui/icons-material/EditNote";
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import { Group, SegmentedControl, Paper, Center, Stack, Button, Text, Box } from "@mantine/core";
|
||||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
|
||||||
import FullscreenIcon from '@mui/icons-material/Fullscreen';
|
|
||||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
|
||||||
import LooksOneIcon from '@mui/icons-material/LooksOne';
|
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
|
||||||
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
|
|
||||||
import LinkIcon from '@mui/icons-material/Link';
|
|
||||||
import CodeIcon from '@mui/icons-material/Code';
|
|
||||||
import TableChartIcon from '@mui/icons-material/TableChart';
|
|
||||||
import IntegrationInstructionsIcon from '@mui/icons-material/IntegrationInstructions';
|
|
||||||
import LockIcon from '@mui/icons-material/Lock';
|
|
||||||
import LockOpenIcon from '@mui/icons-material/LockOpen';
|
|
||||||
import EditNoteIcon from '@mui/icons-material/EditNote';
|
|
||||||
import WorkspacePremiumIcon from '@mui/icons-material/WorkspacePremium';
|
|
||||||
import VerifiedIcon from '@mui/icons-material/Verified';
|
|
||||||
import RemoveModeratorIcon from '@mui/icons-material/RemoveModerator';
|
|
||||||
import SanitizerIcon from '@mui/icons-material/Sanitizer';
|
|
||||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
|
||||||
import DrawIcon from '@mui/icons-material/Draw';
|
|
||||||
import ApprovalIcon from '@mui/icons-material/Approval';
|
|
||||||
import WaterDropIcon from '@mui/icons-material/WaterDrop';
|
|
||||||
import MenuBookIcon from '@mui/icons-material/MenuBook';
|
|
||||||
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
|
|
||||||
import AssignmentIcon from '@mui/icons-material/Assignment';
|
|
||||||
import CollectionsIcon from '@mui/icons-material/Collections';
|
|
||||||
import LayersClearIcon from '@mui/icons-material/LayersClear';
|
|
||||||
import ScannerIcon from '@mui/icons-material/Scanner';
|
|
||||||
import NoteAltIcon from '@mui/icons-material/NoteAlt';
|
|
||||||
import CompareIcon from '@mui/icons-material/Compare';
|
|
||||||
import InfoIcon from '@mui/icons-material/Info';
|
|
||||||
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
|
|
||||||
import InvertColorsIcon from '@mui/icons-material/InvertColors';
|
|
||||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
|
||||||
import PaletteIcon from '@mui/icons-material/Palette';
|
|
||||||
import ZoomInMapIcon from '@mui/icons-material/ZoomInMap';
|
|
||||||
import BuildIcon from '@mui/icons-material/Build';
|
|
||||||
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline';
|
|
||||||
import JavascriptIcon from '@mui/icons-material/Javascript';
|
|
||||||
import SegmentIcon from '@mui/icons-material/Segment';
|
|
||||||
import LayersIcon from '@mui/icons-material/Layers';
|
|
||||||
import GridOnIcon from '@mui/icons-material/GridOn';
|
|
||||||
import AutoStoriesIcon from '@mui/icons-material/AutoStories';
|
|
||||||
import Icon from '@mui/material/Icon';
|
|
||||||
|
|
||||||
|
import FileManager from "../components/FileManager";
|
||||||
import SplitPdfPanel from "../tools/Split";
|
import SplitPdfPanel from "../tools/Split";
|
||||||
import CompressPdfPanel from "../tools/Compress-pdf";
|
import CompressPdfPanel from "../tools/Compress";
|
||||||
|
import MergePdfPanel from "../tools/Merge";
|
||||||
|
import PageEditor from "../components/PageEditor";
|
||||||
|
import Viewer from "../components/Viewer";
|
||||||
|
|
||||||
const toolRegistry = {
|
const toolRegistry = {
|
||||||
"split-pdf": { icon: <PictureAsPdfIcon />, name: "Split PDF", component: SplitPdfPanel },
|
split: { icon: <ContentCutIcon />, name: "Split PDF", component: SplitPdfPanel, view: "viewer" },
|
||||||
"compress-pdf": { icon: <ZoomInMapIcon />, name: "Compress PDF", component: CompressPdfPanel }
|
compress: { icon: <ZoomInMapIcon />, name: "Compress PDF", component: CompressPdfPanel, view: "viewer" },
|
||||||
|
merge: { icon: <AddToPhotosIcon />, name: "Merge PDFs", component: MergePdfPanel, view: "fileManager" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const tools = Object.entries(toolRegistry).map(([id, { icon, name }]) => ({ id, icon, name }));
|
const VIEW_OPTIONS = [
|
||||||
|
{
|
||||||
// Example tool panels
|
label: (
|
||||||
function ToolPanel({ selectedTool }) {
|
<Group gap={4}>
|
||||||
if (!selectedTool) {
|
<VisibilityIcon fontSize="small" />
|
||||||
return (
|
</Group>
|
||||||
<div className="p-2 border rounded bg-white shadow-sm">
|
),
|
||||||
<p className="text-sm">Select a tool to begin interacting with the PDF.</p>
|
value: "viewer",
|
||||||
</div>
|
},
|
||||||
);
|
{
|
||||||
}
|
label: (
|
||||||
return (
|
<Group gap={4}>
|
||||||
<div className="p-2 border rounded bg-white shadow-sm">
|
<EditNoteIcon fontSize="small" />
|
||||||
<h3 className="font-semibold text-sm mb-2">{selectedTool.name}</h3>
|
</Group>
|
||||||
<p className="text-xs text-gray-600">This is the panel for {selectedTool.name}.</p>
|
),
|
||||||
</div>
|
value: "pageEditor",
|
||||||
);
|
},
|
||||||
}
|
{
|
||||||
|
label: (
|
||||||
export default function HomePage() {
|
<Group gap={4}>
|
||||||
const tools = [
|
<InsertDriveFileIcon fontSize="small" />
|
||||||
{ id: "multi-tool", icon: <ConstructionIcon />, name: "Multi-Tool" },
|
</Group>
|
||||||
{ id: "merge-pdfs", icon: <AddToPhotosIcon />, name: "Merge PDFs" },
|
),
|
||||||
{ id: "split-pdf", icon: <ContentCutIcon />, name: "Split PDF" },
|
value: "fileManager",
|
||||||
{ id: "rotate-pdf", icon: <RotateRightIcon />, name: "Rotate Pages" },
|
},
|
||||||
{ id: "crop", icon: <CropIcon />, name: "Crop PDF" },
|
|
||||||
{ id: "pdf-organizer", icon: <FormatListBulletedIcon />, name: "PDF Organizer" },
|
|
||||||
{ id: "remove-pages", icon: <DeleteIcon />, name: "Remove Pages" },
|
|
||||||
{ id: "multi-page-layout", icon: <DashboardIcon />, name: "Page Layout" },
|
|
||||||
{ id: "scale-pages", icon: <FullscreenIcon />, name: "Scale Pages" },
|
|
||||||
{ id: "extract-page", icon: <FileUploadIcon />, name: "Extract Page" },
|
|
||||||
{ id: "pdf-to-single-page", icon: <LooksOneIcon />, name: "PDF to Single Page" },
|
|
||||||
{ id: "img-to-pdf", icon: <PictureAsPdfIcon />, name: "Image to PDF" },
|
|
||||||
{ id: "file-to-pdf", icon: <InsertDriveFileIcon />, name: "File to PDF" },
|
|
||||||
{ id: "url-to-pdf", icon: <LinkIcon />, name: "URL to PDF" },
|
|
||||||
{ id: "html-to-pdf", icon: <CodeIcon />, name: "HTML to PDF" },
|
|
||||||
{ id: "markdown-to-pdf", icon: <IntegrationInstructionsIcon />, name: "Markdown to PDF" },
|
|
||||||
{ id: "pdf-to-img", icon: <CollectionsIcon />, name: "PDF to Image" },
|
|
||||||
{ id: "pdf-to-pdfa", icon: <PictureAsPdfIcon />, name: "PDF to PDF/A" },
|
|
||||||
{ id: "pdf-to-word", icon: <InsertDriveFileIcon />, name: "PDF to Word" },
|
|
||||||
{ id: "pdf-to-presentation", icon: <DashboardIcon />, name: "PDF to Presentation" },
|
|
||||||
{ id: "pdf-to-text", icon: <AssignmentIcon />, name: "PDF to Text" },
|
|
||||||
{ id: "pdf-to-html", icon: <CodeIcon />, name: "PDF to HTML" },
|
|
||||||
{ id: "pdf-to-xml", icon: <CodeIcon />, name: "PDF to XML" },
|
|
||||||
{ id: "pdf-to-csv", icon: <TableChartIcon />, name: "PDF to CSV" },
|
|
||||||
{ id: "pdf-to-markdown", icon: <IntegrationInstructionsIcon />, name: "PDF to Markdown" },
|
|
||||||
{ id: "add-password", icon: <LockIcon />, name: "Add Password" },
|
|
||||||
{ id: "remove-password", icon: <LockOpenIcon />, name: "Remove Password" },
|
|
||||||
{ id: "change-permissions", icon: <LockIcon />, name: "Change Permissions" },
|
|
||||||
{ id: "sign", icon: <EditNoteIcon />, name: "Sign PDF" },
|
|
||||||
{ id: "cert-sign", icon: <WorkspacePremiumIcon />, name: "Certify Signature" },
|
|
||||||
{ id: "validate-signature", icon: <VerifiedIcon />, name: "Validate Signature" },
|
|
||||||
{ id: "remove-cert-sign", icon: <RemoveModeratorIcon />, name: "Remove Cert Signature" },
|
|
||||||
{ id: "sanitize-pdf", icon: <SanitizerIcon />, name: "Sanitize PDF" },
|
|
||||||
{ id: "auto-redact", icon: <VisibilityOffIcon />, name: "Auto Redact" },
|
|
||||||
{ id: "redact", icon: <DrawIcon />, name: "Manual Redact" },
|
|
||||||
{ id: "stamp", icon: <ApprovalIcon />, name: "Add Stamp" },
|
|
||||||
{ id: "add-watermark", icon: <WaterDropIcon />, name: "Add Watermark" },
|
|
||||||
{ id: "view-pdf", icon: <MenuBookIcon />, name: "View PDF" },
|
|
||||||
{ id: "add-page-numbers", icon: <LooksOneIcon />, name: "Add Page Numbers" },
|
|
||||||
{ id: "add-image", icon: <AddPhotoAlternateIcon />, name: "Add Image" },
|
|
||||||
{ id: "change-metadata", icon: <AssignmentIcon />, name: "Change Metadata" },
|
|
||||||
{ id: "ocr-pdf", icon: <LayersIcon />, name: "OCR PDF" },
|
|
||||||
{ id: "extract-images", icon: <CollectionsIcon />, name: "Extract Images" },
|
|
||||||
{ id: "flatten", icon: <LayersClearIcon />, name: "Flatten PDF" },
|
|
||||||
{ id: "remove-blanks", icon: <ScannerIcon />, name: "Remove Blank Pages" },
|
|
||||||
{ id: "remove-annotations", icon: <NoteAltIcon />, name: "Remove Annotations" },
|
|
||||||
{ id: "compare", icon: <CompareIcon />, name: "Compare PDFs" },
|
|
||||||
{ id: "get-info-on-pdf", icon: <InfoIcon />, name: "PDF Info" },
|
|
||||||
{ id: "remove-image-pdf", icon: <HighlightOffIcon />, name: "Remove Images from PDF" },
|
|
||||||
{ id: "replace-and-invert-color-pdf", icon: <InvertColorsIcon />, name: "Invert Colors" },
|
|
||||||
{ id: "unlock-pdf-forms", icon: <LayersIcon />, name: "Unlock PDF Forms" },
|
|
||||||
{ id: "pipeline", icon: <AccountTreeIcon />, name: "Pipeline" },
|
|
||||||
{ id: "adjust-contrast", icon: <PaletteIcon />, name: "Adjust Contrast" },
|
|
||||||
{ id: "compress-pdf", icon: <ZoomInMapIcon />, name: "Compress PDF" },
|
|
||||||
{ id: "extract-image-scans", icon: <ScannerIcon />, name: "Extract Image Scans" },
|
|
||||||
{ id: "repair", icon: <BuildIcon />, name: "Repair PDF" },
|
|
||||||
{ id: "auto-rename", icon: <DriveFileRenameOutlineIcon />, name: "Auto Rename" },
|
|
||||||
{ id: "show-javascript", icon: <JavascriptIcon />, name: "Show JavaScript" },
|
|
||||||
{ id: "overlay-pdf", icon: <LayersIcon />, name: "Overlay PDF" },
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const [selectedTool, setSelectedTool] = useState(null);
|
export default function HomePage() {
|
||||||
const [search, setSearch] = useState("");
|
const [selectedToolKey, setSelectedToolKey] = useState("split");
|
||||||
const [pdfFile, setPdfFile] = useState(null);
|
const [currentView, setCurrentView] = useState("viewer");
|
||||||
const SelectedComponent = selectedTool ? toolRegistry[selectedTool.id]?.component : null;
|
const [pdfFile, setPdfFile] = useState(null);
|
||||||
const [downloadUrl, setDownloadUrl] = useState(null);
|
const [files, setFiles] = useState([]);
|
||||||
|
const [downloadUrl, setDownloadUrl] = useState(null);
|
||||||
|
|
||||||
const filteredTools = tools.filter(tool =>
|
const selectedTool = toolRegistry[selectedToolKey];
|
||||||
tool.name.toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleFileUpload(e) {
|
return (
|
||||||
const file = e.target.files[0];
|
<Group align="flex-start" spacing={0} style={{ minHeight: "100vh" }}>
|
||||||
if (file && file.type === "application/pdf") {
|
{/* Left: Tool Picker */}
|
||||||
const fileUrl = URL.createObjectURL(file);
|
<Box
|
||||||
setPdfFile({ file, url: fileUrl });
|
style={{
|
||||||
}
|
width: 220,
|
||||||
}
|
background: "#f8f9fa",
|
||||||
|
borderRight: "1px solid #e9ecef",
|
||||||
return ( <div className="flex h-screen overflow-hidden">
|
minHeight: "100vh",
|
||||||
{/* Left Sidebar */}
|
padding: 16,
|
||||||
<div className="w-64 bg-gray-100 p-4 flex flex-col space-y-2 overflow-y-auto border-r">
|
}}
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search tools..."
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
className="mb-3 px-2 py-1 border rounded text-sm"
|
|
||||||
/>
|
|
||||||
{filteredTools.map(tool => (
|
|
||||||
<button
|
|
||||||
key={tool.id}
|
|
||||||
title={tool.name}
|
|
||||||
onClick={() => setSelectedTool(tool)}
|
|
||||||
className="flex items-center space-x-3 p-2 hover:bg-gray-200 rounded text-left"
|
|
||||||
>
|
>
|
||||||
<div className="text-xl leading-none flex items-center justify-center h-6 w-6">
|
<Text size="lg" weight={500} mb="md">
|
||||||
{tool.icon}
|
Tools
|
||||||
</div>
|
</Text>
|
||||||
<span className="text-sm font-medium">{tool.name}</span>
|
<Stack spacing="sm">
|
||||||
</button>
|
{Object.entries(toolRegistry).map(([id, { icon, name }]) => (
|
||||||
))}
|
<Button
|
||||||
</div>
|
key={id}
|
||||||
|
variant={selectedToolKey === id ? "filled" : "subtle"}
|
||||||
|
leftIcon={icon}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedToolKey(id);
|
||||||
|
if (toolRegistry[id].view) setCurrentView(toolRegistry[id].view);
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
size="md"
|
||||||
|
radius="md"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Central PDF Viewer Area */}
|
{/* Middle: Main View (Viewer, Editor, Manager) */}
|
||||||
<div className="flex-1 bg-white flex items-center justify-center overflow-hidden">
|
<Box
|
||||||
<div className="w-full h-full max-w-5xl max-h-[95vh] border rounded shadow-md bg-gray-50 flex items-center justify-center">
|
style={{
|
||||||
{!pdfFile ? (
|
flex: 1,
|
||||||
<label className="cursor-pointer text-blue-600 underline">
|
minWidth: 0,
|
||||||
Click to upload a PDF
|
padding: 24,
|
||||||
<input
|
background: "#fff",
|
||||||
type="file"
|
minHeight: "100vh",
|
||||||
accept="application/pdf"
|
position: "relative",
|
||||||
onChange={handleFileUpload}
|
}}
|
||||||
className="hidden"
|
>
|
||||||
/>
|
<Center>
|
||||||
</label>
|
<Paper
|
||||||
) : (
|
radius="xl"
|
||||||
<iframe
|
shadow="sm"
|
||||||
src={pdfFile.url}
|
p={4}
|
||||||
title="PDF Viewer"
|
style={{
|
||||||
className="w-full h-full border-none"
|
display: "inline-block",
|
||||||
/>
|
marginTop: 8,
|
||||||
)}
|
marginBottom: 24,
|
||||||
</div>
|
background: "#f8f9fa",
|
||||||
</div>
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SegmentedControl
|
||||||
|
data={VIEW_OPTIONS}
|
||||||
|
value={currentView}
|
||||||
|
onChange={setCurrentView}
|
||||||
|
color="blue"
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Center>
|
||||||
|
<Box>
|
||||||
|
{currentView === "viewer" && (
|
||||||
|
<Viewer
|
||||||
|
file={pdfFile}
|
||||||
|
setFile={setPdfFile}
|
||||||
|
downloadUrl={downloadUrl}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{currentView === "pageEditor" && (
|
||||||
|
<PageEditor
|
||||||
|
file={pdfFile}
|
||||||
|
setFile={setPdfFile}
|
||||||
|
downloadUrl={downloadUrl}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{currentView === "fileManager" && (
|
||||||
|
<FileManager
|
||||||
|
files={files}
|
||||||
|
setFiles={setFiles}
|
||||||
|
setPdfFile={setPdfFile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Right Sidebar: Tool Interactions */}
|
{/* Right: Tool Interaction */}
|
||||||
<div className="w-72 bg-gray-50 p-4 border-l overflow-y-auto">
|
<Box
|
||||||
<h2 className="text-lg font-semibold mb-4">Tool Panel</h2>
|
style={{
|
||||||
<div className="space-y-3">
|
width: 380,
|
||||||
{SelectedComponent ? (
|
background: "#f8f9fa",
|
||||||
<SelectedComponent file={pdfFile} downloadUrl setDownloadUrl />
|
borderLeft: "1px solid #e9ecef",
|
||||||
) : selectedTool ? (
|
minHeight: "100vh",
|
||||||
<div className="p-2 border rounded bg-white shadow-sm">
|
padding: 24,
|
||||||
<h3 className="font-semibold text-sm mb-2">{selectedTool.name}</h3>
|
}}
|
||||||
<p className="text-xs text-gray-600">This is the panel for {selectedTool.name}.</p>
|
>
|
||||||
</div>
|
{selectedTool && selectedTool.component && (
|
||||||
) : (
|
<Paper p="md" radius="md" shadow="xs">
|
||||||
<div className="p-2 border rounded bg-white shadow-sm">
|
{React.createElement(selectedTool.component, {
|
||||||
<p className="text-sm">Select a tool to begin interacting with the PDF.</p>
|
file: pdfFile,
|
||||||
</div>
|
setPdfFile,
|
||||||
)}
|
files,
|
||||||
</div>
|
setFiles,
|
||||||
</div>
|
downloadUrl,
|
||||||
</div>
|
setDownloadUrl,
|
||||||
);
|
})}
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export default function CompressPdfPanel({file}) {
|
|
||||||
const [optimizeLevel, setOptimizeLevel] = useState("5");
|
|
||||||
const [grayscale, setGrayscale] = useState(false);
|
|
||||||
const [expectedOutputSize, setExpectedOutputSize] = useState("");
|
|
||||||
const [status, setStatus] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
setStatus("Please select a file.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file.file);
|
|
||||||
formData.append("optimizeLevel", optimizeLevel);
|
|
||||||
formData.append("grayscale", grayscale);
|
|
||||||
if (expectedOutputSize) {
|
|
||||||
formData.append("expectedOutputSize", expectedOutputSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStatus("Compressing...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post("/api/v1/misc/compress-pdf", formData, {
|
|
||||||
responseType: "blob",
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute("download", "compressed.pdf");
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
|
|
||||||
setStatus("Download ready!");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
setStatus("Failed to compress PDF.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4 text-sm">
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Compression Level (1-9)</label>
|
|
||||||
<select
|
|
||||||
value={optimizeLevel}
|
|
||||||
onChange={(e) => setOptimizeLevel(e.target.value)}
|
|
||||||
className="w-full border px-2 py-1 rounded"
|
|
||||||
>
|
|
||||||
{[...Array(9)].map((_, i) => (
|
|
||||||
<option key={i + 1} value={i + 1}>{i + 1}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="grayscale"
|
|
||||||
checked={grayscale}
|
|
||||||
onChange={(e) => setGrayscale(e.target.checked)}
|
|
||||||
className="mr-2"
|
|
||||||
/>
|
|
||||||
<label htmlFor="grayscale">Convert images to grayscale</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium">Expected Output Size (e.g. 2MB)</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={expectedOutputSize}
|
|
||||||
onChange={(e) => setExpectedOutputSize(e.target.value)}
|
|
||||||
className="w-full border px-2 py-1 rounded"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
|
|
||||||
Compress PDF
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{status && <p className="text-xs text-gray-600 mt-2">{status}</p>}
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
113
frontend/src/tools/Compress.js
Normal file
113
frontend/src/tools/Compress.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
|
||||||
|
|
||||||
|
export default function CompressPdfPanel({ files = [], setDownloadUrl, setLoading }) {
|
||||||
|
const [selected, setSelected] = useState(files.map(() => false));
|
||||||
|
const [compressionLevel, setCompressionLevel] = useState(5); // 1-9, default 5
|
||||||
|
const [grayscale, setGrayscale] = useState(false);
|
||||||
|
const [removeMetadata, setRemoveMetadata] = useState(false);
|
||||||
|
const [expectedSize, setExpectedSize] = useState("");
|
||||||
|
const [aggressive, setAggressive] = useState(false);
|
||||||
|
const [localLoading, setLocalLoading] = useState(false);
|
||||||
|
|
||||||
|
// Update selection state if files prop changes
|
||||||
|
React.useEffect(() => {
|
||||||
|
setSelected(files.map(() => false));
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const handleCheckbox = idx => {
|
||||||
|
setSelected(sel => sel.map((v, i) => (i === idx ? !v : v)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCompress = async () => {
|
||||||
|
const selectedFiles = files.filter((_, i) => selected[i]);
|
||||||
|
if (selectedFiles.length === 0) return;
|
||||||
|
setLocalLoading(true);
|
||||||
|
setLoading?.(true);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
selectedFiles.forEach(file => formData.append("fileInput", file));
|
||||||
|
formData.append("compressionLevel", compressionLevel);
|
||||||
|
formData.append("grayscale", grayscale);
|
||||||
|
formData.append("removeMetadata", removeMetadata);
|
||||||
|
formData.append("aggressive", aggressive);
|
||||||
|
if (expectedSize) formData.append("expectedSize", expectedSize);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/v1/general/compress-pdf", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
const blob = await res.blob();
|
||||||
|
setDownloadUrl(URL.createObjectURL(blob));
|
||||||
|
} finally {
|
||||||
|
setLocalLoading(false);
|
||||||
|
setLoading?.(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper shadow="xs" p="md" radius="md" withBorder>
|
||||||
|
<Stack>
|
||||||
|
<Text weight={500} mb={4}>Select files to compress:</Text>
|
||||||
|
<Stack spacing={4}>
|
||||||
|
{files.length === 0 && <Text color="dimmed" size="sm">No files loaded.</Text>}
|
||||||
|
{files.map((file, idx) => (
|
||||||
|
<Checkbox
|
||||||
|
key={file.name + idx}
|
||||||
|
label={file.name}
|
||||||
|
checked={selected[idx] || false}
|
||||||
|
onChange={() => handleCheckbox(idx)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={4} mb={14}>
|
||||||
|
<Text size="sm" style={{ minWidth: 140 }}>Compression Level</Text>
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={9}
|
||||||
|
step={1}
|
||||||
|
value={compressionLevel}
|
||||||
|
onChange={setCompressionLevel}
|
||||||
|
marks={[
|
||||||
|
{ value: 1, label: "1" },
|
||||||
|
{ value: 5, label: "5" },
|
||||||
|
{ value: 9, label: "9" },
|
||||||
|
]}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</Stack >
|
||||||
|
<Checkbox
|
||||||
|
label="Convert images to grayscale"
|
||||||
|
checked={grayscale}
|
||||||
|
onChange={e => setGrayscale(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Remove PDF metadata"
|
||||||
|
checked={removeMetadata}
|
||||||
|
onChange={e => setRemoveMetadata(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Aggressive compression (may reduce quality)"
|
||||||
|
checked={aggressive}
|
||||||
|
onChange={e => setAggressive(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label="Expected output size (e.g. 2MB, 500KB)"
|
||||||
|
placeholder="Optional"
|
||||||
|
value={expectedSize}
|
||||||
|
onChange={e => setExpectedSize(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleCompress}
|
||||||
|
loading={localLoading}
|
||||||
|
disabled={selected.every(v => !v)}
|
||||||
|
fullWidth
|
||||||
|
mt="md"
|
||||||
|
>
|
||||||
|
Compress Selected PDF{selected.filter(Boolean).length > 1 ? "s" : ""}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
101
frontend/src/tools/Merge.js
Normal file
101
frontend/src/tools/Merge.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export default function MergePdfPanel({ files, setDownloadUrl }) {
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||||
|
const [downloadUrl, setLocalDownloadUrl] = useState(null); // Local state for download URL
|
||||||
|
const [isLoading, setIsLoading] = useState(false); // Loading state
|
||||||
|
const [errorMessage, setErrorMessage] = useState(null); // Error message state
|
||||||
|
|
||||||
|
// Sync selectedFiles with files whenever files change
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedFiles(files.map(() => true)); // Select all files by default
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const handleMerge = async () => {
|
||||||
|
const filesToMerge = files.filter((_, index) => selectedFiles[index]);
|
||||||
|
|
||||||
|
if (filesToMerge.length < 2) {
|
||||||
|
alert("Please select at least two PDFs to merge.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
filesToMerge.forEach((file) => formData.append("fileInput", file)); // Use "fileInput" as the key
|
||||||
|
|
||||||
|
setIsLoading(true); // Start loading
|
||||||
|
setErrorMessage(null); // Clear previous errors
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/v1/general/merge-pdfs", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Failed to merge PDFs: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
const downloadUrl = URL.createObjectURL(blob);
|
||||||
|
setDownloadUrl(downloadUrl); // Pass to parent component
|
||||||
|
setLocalDownloadUrl(downloadUrl); // Store locally for download button
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error merging PDFs:", error);
|
||||||
|
setErrorMessage(error.message); // Set error message
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false); // Stop loading
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckboxChange = (index) => {
|
||||||
|
setSelectedFiles((prevSelectedFiles) =>
|
||||||
|
prevSelectedFiles.map((selected, i) => (i === index ? !selected : selected))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-semibold text-lg">Merge PDFs</h3>
|
||||||
|
<ul className="list-disc pl-5 text-sm">
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<li key={index} className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedFiles[index]}
|
||||||
|
onChange={() => handleCheckboxChange(index)}
|
||||||
|
className="form-checkbox"
|
||||||
|
/>
|
||||||
|
<span>{file.name}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{files.filter((_, index) => selectedFiles[index]).length < 2 && (
|
||||||
|
<p className="text-sm text-red-500">
|
||||||
|
Please select at least two PDFs to merge.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleMerge}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
|
disabled={files.filter((_, index) => selectedFiles[index]).length < 2 || isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Merging..." : "Merge PDFs"}
|
||||||
|
</button>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500 mt-2">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{downloadUrl && (
|
||||||
|
<a
|
||||||
|
href={downloadUrl}
|
||||||
|
download="merged.pdf"
|
||||||
|
className="block mt-4 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 text-center"
|
||||||
|
>
|
||||||
|
Download Merged PDF
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,10 +1,33 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import {
|
||||||
|
Button,
|
||||||
|
Select,
|
||||||
|
TextInput,
|
||||||
|
Checkbox,
|
||||||
|
Notification,
|
||||||
|
Stack,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
|
||||||
export default function SplitPdfPanel({ file, downloadUrl, setDownloadUrl }) {
|
export default function SplitPdfPanel({ file, downloadUrl, setDownloadUrl }) {
|
||||||
const [mode, setMode] = useState("byPages");
|
const [mode, setMode] = useState("byPages");
|
||||||
|
const [pageNumbers, setPageNumbers] = useState("");
|
||||||
|
|
||||||
|
const [horizontalDivisions, setHorizontalDivisions] = useState("0");
|
||||||
|
const [verticalDivisions, setVerticalDivisions] = useState("1");
|
||||||
|
const [mergeSections, setMergeSections] = useState(false);
|
||||||
|
|
||||||
|
const [splitType, setSplitType] = useState("size");
|
||||||
|
const [splitValue, setSplitValue] = useState("");
|
||||||
|
|
||||||
|
const [bookmarkLevel, setBookmarkLevel] = useState("0");
|
||||||
|
const [includeMetadata, setIncludeMetadata] = useState(false);
|
||||||
|
const [allowDuplicates, setAllowDuplicates] = useState(false);
|
||||||
|
|
||||||
const [status, setStatus] = useState("");
|
const [status, setStatus] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState(null);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -17,175 +40,168 @@ export default function SplitPdfPanel({ file, downloadUrl, setDownloadUrl }) {
|
|||||||
formData.append("fileInput", file.file);
|
formData.append("fileInput", file.file);
|
||||||
|
|
||||||
let endpoint = "";
|
let endpoint = "";
|
||||||
if (mode === "byPages") {
|
|
||||||
const pageNumbers = document.getElementById("pagesInput").value;
|
switch (mode) {
|
||||||
formData.append("pageNumbers", pageNumbers);
|
case "byPages":
|
||||||
endpoint = "/api/v1/general/split-pages";
|
formData.append("pageNumbers", pageNumbers);
|
||||||
} else if (mode === "bySections") {
|
endpoint = "/api/v1/general/split-pages";
|
||||||
const horizontal = document.getElementById("horizontalDivisions").value;
|
break;
|
||||||
const vertical = document.getElementById("verticalDivisions").value;
|
case "bySections":
|
||||||
const merge = document.getElementById("merge").checked;
|
formData.append("horizontalDivisions", horizontalDivisions);
|
||||||
formData.append("horizontalDivisions", horizontal);
|
formData.append("verticalDivisions", verticalDivisions);
|
||||||
formData.append("verticalDivisions", vertical);
|
formData.append("merge", mergeSections);
|
||||||
formData.append("merge", merge);
|
endpoint = "/api/v1/general/split-pdf-by-sections";
|
||||||
endpoint = "/api/v1/general/split-pdf-by-sections";
|
break;
|
||||||
} else if (mode === "bySizeOrCount") {
|
case "bySizeOrCount":
|
||||||
const splitType = document.getElementById("splitType").value;
|
formData.append("splitType", splitType === "size" ? 0 : splitType === "pages" ? 1 : 2);
|
||||||
const splitValue = document.getElementById("splitValue").value;
|
formData.append("splitValue", splitValue);
|
||||||
formData.append("splitType", splitType === "size" ? 0 : splitType === "pages" ? 1 : 2);
|
endpoint = "/api/v1/general/split-by-size-or-count";
|
||||||
formData.append("splitValue", splitValue);
|
break;
|
||||||
endpoint = "/api/v1/general/split-by-size-or-count";
|
case "byChapters":
|
||||||
} else if (mode === "byChapters") {
|
formData.append("bookmarkLevel", bookmarkLevel);
|
||||||
const bookmarkLevel = document.getElementById("bookmarkLevel").value;
|
formData.append("includeMetadata", includeMetadata);
|
||||||
const includeMetadata = document.getElementById("includeMetadata").checked;
|
formData.append("allowDuplicates", allowDuplicates);
|
||||||
const allowDuplicates = document.getElementById("allowDuplicates").checked;
|
endpoint = "/api/v1/general/split-pdf-by-chapters";
|
||||||
formData.append("bookmarkLevel", bookmarkLevel);
|
break;
|
||||||
formData.append("includeMetadata", includeMetadata);
|
default:
|
||||||
formData.append("allowDuplicates", allowDuplicates);
|
return;
|
||||||
endpoint = "/api/v1/general/split-pdf-by-chapters";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus("Processing split...");
|
setStatus("Processing split...");
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute("download", "split_output.zip");
|
|
||||||
document.body.appendChild(link);
|
|
||||||
const blob = new Blob([response.data], { type: "application/zip" });
|
const blob = new Blob([response.data], { type: "application/zip" });
|
||||||
const url = window.URL.createObjectURL(blob);
|
const url = window.URL.createObjectURL(blob);
|
||||||
setDownloadUrl(url);
|
setDownloadUrl(url);
|
||||||
setStatus("Download ready.");
|
setStatus("Download ready.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
setErrorMessage(error.response?.data || "An error occurred while splitting the PDF.");
|
||||||
setStatus("Split failed.");
|
setStatus("Split failed.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="p-2 border rounded bg-white shadow-sm space-y-4 text-sm">
|
<form onSubmit={handleSubmit} >
|
||||||
<h3 className="font-semibold">Split PDF</h3>
|
<h3 className="font-semibold">Split PDF</h3>
|
||||||
|
|
||||||
<div>
|
<Select
|
||||||
<label className="block mb-1 font-medium">Split Mode</label>
|
label="Split Mode"
|
||||||
<select
|
value={mode}
|
||||||
value={mode}
|
onChange={setMode}
|
||||||
onChange={(e) => setMode(e.target.value)}
|
data={[
|
||||||
className="w-full border px-2 py-1 rounded"
|
{ value: "byPages", label: "Split by Pages (e.g. 1,3,5-10)" },
|
||||||
>
|
{ value: "bySections", label: "Split by Grid Sections" },
|
||||||
<option value="byPages">Split by Pages (e.g. 1,3,5-10)</option>
|
{ value: "bySizeOrCount", label: "Split by Size or Count" },
|
||||||
<option value="bySections">Split by Grid Sections</option>
|
{ value: "byChapters", label: "Split by Chapters" },
|
||||||
<option value="bySizeOrCount">Split by Size or Count</option>
|
]}
|
||||||
<option value="byChapters">Split by Chapters</option>
|
/>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{mode === "byPages" && (
|
{mode === "byPages" && (
|
||||||
<div>
|
<TextInput
|
||||||
<label className="block font-medium mb-1">Pages</label>
|
label="Pages"
|
||||||
<input
|
placeholder="e.g. 1,3,5-10"
|
||||||
type="text"
|
value={pageNumbers}
|
||||||
id="pagesInput"
|
onChange={(e) => setPageNumbers(e.target.value)}
|
||||||
className="w-full border px-2 py-1 rounded"
|
/>
|
||||||
placeholder="e.g. 1,3,5-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mode === "bySections" && (
|
{mode === "bySections" && (
|
||||||
<div className="space-y-2">
|
<Stack spacing="sm">
|
||||||
<div>
|
<TextInput
|
||||||
<label className="block font-medium mb-1">Horizontal Divisions</label>
|
label="Horizontal Divisions"
|
||||||
<input
|
type="number"
|
||||||
type="number"
|
min="0"
|
||||||
id="horizontalDivisions"
|
max="300"
|
||||||
className="w-full border px-2 py-1 rounded"
|
value={horizontalDivisions}
|
||||||
min="0"
|
onChange={(e) => setHorizontalDivisions(e.target.value)}
|
||||||
max="300"
|
/>
|
||||||
defaultValue="0"
|
<TextInput
|
||||||
/>
|
label="Vertical Divisions"
|
||||||
</div>
|
type="number"
|
||||||
<div>
|
min="0"
|
||||||
<label className="block font-medium mb-1">Vertical Divisions</label>
|
max="300"
|
||||||
<input
|
value={verticalDivisions}
|
||||||
type="number"
|
onChange={(e) => setVerticalDivisions(e.target.value)}
|
||||||
id="verticalDivisions"
|
/>
|
||||||
className="w-full border px-2 py-1 rounded"
|
<Checkbox
|
||||||
min="0"
|
label="Merge sections into one PDF"
|
||||||
max="300"
|
checked={mergeSections}
|
||||||
defaultValue="1"
|
onChange={(e) => setMergeSections(e.currentTarget.checked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Stack>
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input type="checkbox" id="merge" />
|
|
||||||
<label htmlFor="merge">Merge sections into one PDF</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mode === "bySizeOrCount" && (
|
{mode === "bySizeOrCount" && (
|
||||||
<div className="space-y-2">
|
<Stack spacing="sm">
|
||||||
<div>
|
<Select
|
||||||
<label className="block font-medium mb-1">Split Type</label>
|
label="Split Type"
|
||||||
<select id="splitType" className="w-full border px-2 py-1 rounded">
|
value={splitType}
|
||||||
<option value="size">By Size</option>
|
onChange={setSplitType}
|
||||||
<option value="pages">By Page Count</option>
|
data={[
|
||||||
<option value="docs">By Document Count</option>
|
{ value: "size", label: "By Size" },
|
||||||
</select>
|
{ value: "pages", label: "By Page Count" },
|
||||||
</div>
|
{ value: "docs", label: "By Document Count" },
|
||||||
<div>
|
]}
|
||||||
<label className="block font-medium mb-1">Split Value</label>
|
/>
|
||||||
<input
|
<TextInput
|
||||||
type="text"
|
label="Split Value"
|
||||||
id="splitValue"
|
placeholder="e.g. 10MB or 5 pages"
|
||||||
className="w-full border px-2 py-1 rounded"
|
value={splitValue}
|
||||||
placeholder="e.g. 10MB or 5 pages"
|
onChange={(e) => setSplitValue(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Stack>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{mode === "byChapters" && (
|
{mode === "byChapters" && (
|
||||||
<div className="space-y-2">
|
<Stack spacing="sm">
|
||||||
<div>
|
<TextInput
|
||||||
<label className="block font-medium mb-1">Bookmark Level</label>
|
label="Bookmark Level"
|
||||||
<input
|
type="number"
|
||||||
type="number"
|
value={bookmarkLevel}
|
||||||
id="bookmarkLevel"
|
onChange={(e) => setBookmarkLevel(e.target.value)}
|
||||||
className="w-full border px-2 py-1 rounded"
|
/>
|
||||||
defaultValue="0"
|
<Checkbox
|
||||||
min="0"
|
label="Include Metadata"
|
||||||
/>
|
checked={includeMetadata}
|
||||||
</div>
|
onChange={(e) => setIncludeMetadata(e.currentTarget.checked)}
|
||||||
<div className="flex items-center space-x-2">
|
/>
|
||||||
<input type="checkbox" id="includeMetadata" />
|
<Checkbox
|
||||||
<label htmlFor="includeMetadata">Include Metadata</label>
|
label="Allow Duplicate Bookmarks"
|
||||||
</div>
|
checked={allowDuplicates}
|
||||||
<div className="flex items-center space-x-2">
|
onChange={(e) => setAllowDuplicates(e.currentTarget.checked)}
|
||||||
<input type="checkbox" id="allowDuplicates" />
|
/>
|
||||||
<label htmlFor="allowDuplicates">Allow Duplicate Bookmarks</label>
|
</Stack>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded mt-2">
|
<Button type="submit" loading={isLoading} fullWidth>
|
||||||
Split PDF
|
{isLoading ? "Processing..." : "Split PDF"}
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
{status && <p className="text-xs text-gray-600">{status}</p>}
|
{status && <p className="text-xs text-gray-600">{status}</p>}
|
||||||
|
|
||||||
{status === "Download ready." && downloadUrl && (
|
{errorMessage && (
|
||||||
<a
|
<Notification color="red" title="Error" onClose={() => setErrorMessage(null)}>
|
||||||
href={downloadUrl}
|
{errorMessage}
|
||||||
download="split_output.zip"
|
</Notification>
|
||||||
className="inline-flex items-center bg-green-600 text-white px-4 py-2 rounded shadow hover:bg-green-700 transition mt-2"
|
)}
|
||||||
>
|
|
||||||
<DownloadIcon className="mr-2" />
|
|
||||||
Download Split PDF
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
{status === "Download ready." && downloadUrl && (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href={downloadUrl}
|
||||||
|
download="split_output.zip"
|
||||||
|
leftIcon={<DownloadIcon />}
|
||||||
|
color="green"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
Download Split PDF
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false,
|
||||||
|
},
|
||||||
content: [
|
content: [
|
||||||
"./src/**/*.{js,jsx,ts,tsx}"
|
"./src/**/*.{js,jsx,ts,tsx}"
|
||||||
],
|
],
|
||||||
|
264
package-lock.json
generated
264
package-lock.json
generated
@ -1,264 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Stirling-PDF",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"devDependencies": {
|
|
||||||
"autoprefixer": "^10.4.21",
|
|
||||||
"postcss": "^8.5.3",
|
|
||||||
"tailwindcss": "^4.1.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/autoprefixer": {
|
|
||||||
"version": "10.4.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
|
||||||
"integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/postcss/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"browserslist": "^4.24.4",
|
|
||||||
"caniuse-lite": "^1.0.30001702",
|
|
||||||
"fraction.js": "^4.3.7",
|
|
||||||
"normalize-range": "^0.1.2",
|
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"postcss-value-parser": "^4.2.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"autoprefixer": "bin/autoprefixer"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12 || >=14"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"postcss": "^8.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/browserslist": {
|
|
||||||
"version": "4.24.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
|
||||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/browserslist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"caniuse-lite": "^1.0.30001716",
|
|
||||||
"electron-to-chromium": "^1.5.149",
|
|
||||||
"node-releases": "^2.0.19",
|
|
||||||
"update-browserslist-db": "^1.1.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"browserslist": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/caniuse-lite": {
|
|
||||||
"version": "1.0.30001718",
|
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
|
|
||||||
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/browserslist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "CC-BY-4.0"
|
|
||||||
},
|
|
||||||
"node_modules/electron-to-chromium": {
|
|
||||||
"version": "1.5.152",
|
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz",
|
|
||||||
"integrity": "sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/escalade": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fraction.js": {
|
|
||||||
"version": "4.3.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
|
||||||
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://github.com/sponsors/rawify"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nanoid": {
|
|
||||||
"version": "3.3.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"nanoid": "bin/nanoid.cjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-releases": {
|
|
||||||
"version": "2.0.19",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
|
||||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/normalize-range": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
|
|
||||||
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/postcss": {
|
|
||||||
"version": "8.5.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
|
||||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/postcss/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"nanoid": "^3.3.8",
|
|
||||||
"picocolors": "^1.1.1",
|
|
||||||
"source-map-js": "^1.2.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^10 || ^12 || >=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/postcss-value-parser": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tailwindcss": {
|
|
||||||
"version": "4.1.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.6.tgz",
|
|
||||||
"integrity": "sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/update-browserslist-db": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/browserslist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tidelift",
|
|
||||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/ai"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"escalade": "^3.2.0",
|
|
||||||
"picocolors": "^1.1.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"update-browserslist-db": "cli.js"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"browserslist": ">= 4.21.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"devDependencies": {
|
|
||||||
"autoprefixer": "^10.4.21",
|
|
||||||
"postcss": "^8.5.3",
|
|
||||||
"tailwindcss": "^4.1.6"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,156 +1,72 @@
|
|||||||
import React, { useState } from "react";
|
<!DOCTYPE html>
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||||
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
|
|
||||||
const tools = [
|
<head>
|
||||||
{ id: "split-pdf", icon: <PictureAsPdfIcon />, name: "Split PDF" }
|
<th:block th:insert="~{fragments/common :: head(title=#{splitByChapters.title}, header=#{splitByChapters.header})}">
|
||||||
];
|
</th:block>
|
||||||
|
</head>
|
||||||
|
|
||||||
function SplitPdfPanel() {
|
<body>
|
||||||
const [mode, setMode] = useState("byPages");
|
<div id="page-container">
|
||||||
return (
|
<div id="content-wrap">
|
||||||
<div className="p-2 border rounded bg-white shadow-sm space-y-4 text-sm">
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
<h3 className="font-semibold">Split PDF</h3>
|
<br><br>
|
||||||
|
<div class="container">
|
||||||
<div>
|
<div class="row justify-content-center">
|
||||||
<label className="block mb-1 font-medium">Split Mode</label>
|
<div class="col-md-6 bg-card">
|
||||||
<select
|
<div class="tool-header">
|
||||||
value={mode}
|
<svg class="material-symbols-rounded tool-header-icon advance">
|
||||||
onChange={(e) => setMode(e.target.value)}
|
<use xlink:href="/images/split-chapters.svg#icon-split-chapters"></use>
|
||||||
className="w-full border px-2 py-1 rounded"
|
</svg>
|
||||||
>
|
<span class="tool-header-text" th:text="#{splitByChapters.header}"></span>
|
||||||
<option value="byPages">Split by Pages (e.g. 1,3,5-10)</option>
|
|
||||||
<option value="bySections">Split by Grid Sections</option>
|
|
||||||
<option value="bySizeOrCount">Split by Size or Count</option>
|
|
||||||
<option value="byChapters">Split by Chapters</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{mode === "byPages" && (
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium mb-1">Pages</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="w-full border px-2 py-1 rounded"
|
|
||||||
placeholder="e.g. 1,3,5-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === "bySections" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium mb-1">Horizontal Divisions</label>
|
|
||||||
<input type="number" className="w-full border px-2 py-1 rounded" min="0" max="300" defaultValue="0" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium mb-1">Vertical Divisions</label>
|
|
||||||
<input type="number" className="w-full border px-2 py-1 rounded" min="0" max="300" defaultValue="1" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input type="checkbox" id="merge" />
|
|
||||||
<label htmlFor="merge">Merge sections into one PDF</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === "bySizeOrCount" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium mb-1">Split Type</label>
|
|
||||||
<select className="w-full border px-2 py-1 rounded">
|
|
||||||
<option value="size">By Size</option>
|
|
||||||
<option value="pages">By Page Count</option>
|
|
||||||
<option value="docs">By Document Count</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium mb-1">Split Value</label>
|
|
||||||
<input type="text" className="w-full border px-2 py-1 rounded" placeholder="e.g. 10MB or 5 pages" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === "byChapters" && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div>
|
|
||||||
<label className="block font-medium mb-1">Bookmark Level</label>
|
|
||||||
<input type="number" className="w-full border px-2 py-1 rounded" defaultValue="0" min="0" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input type="checkbox" id="includeMetadata" />
|
|
||||||
<label htmlFor="includeMetadata">Include Metadata</label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input type="checkbox" id="allowDuplicates" />
|
|
||||||
<label htmlFor="allowDuplicates">Allow Duplicate Bookmarks</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button className="bg-blue-600 text-white px-4 py-2 rounded mt-2">Split PDF</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HomePage() {
|
|
||||||
const [selectedTool, setSelectedTool] = useState(null);
|
|
||||||
const [search, setSearch] = useState("");
|
|
||||||
|
|
||||||
const filteredTools = tools.filter(tool =>
|
|
||||||
tool.name.toLowerCase().includes(search.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen overflow-hidden">
|
|
||||||
{/* Left Sidebar */}
|
|
||||||
<div className="w-64 bg-gray-100 p-4 flex flex-col space-y-2 overflow-y-auto border-r">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search tools..."
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
className="mb-3 px-2 py-1 border rounded text-sm"
|
|
||||||
/>
|
|
||||||
{filteredTools.map(tool => (
|
|
||||||
<button
|
|
||||||
key={tool.id}
|
|
||||||
title={tool.name}
|
|
||||||
onClick={() => setSelectedTool(tool)}
|
|
||||||
className="flex items-center space-x-3 p-2 hover:bg-gray-200 rounded text-left"
|
|
||||||
>
|
|
||||||
<div className="text-xl leading-none flex items-center justify-center h-6 w-6">
|
|
||||||
{tool.icon}
|
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">{tool.name}</span>
|
<form th:action="@{'/api/v1/general/split-pdf-by-chapters'}" method="post" enctype="multipart/form-data">
|
||||||
</button>
|
<div
|
||||||
))}
|
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Central PDF Viewer Area */}
|
<div class="mb-3">
|
||||||
<div className="flex-1 bg-white flex items-center justify-center overflow-hidden">
|
<label for="bookmarkLevel" th:text="#{splitByChapters.bookmarkLevel}"></label>
|
||||||
<div className="w-full h-full max-w-5xl max-h-[95vh] border rounded shadow-md bg-gray-50 flex items-center justify-center">
|
<input type="number" class="form-control" id="bookmarkLevel" name="bookmarkLevel" min="0" value="0"
|
||||||
<span className="text-gray-400 text-lg">PDF Viewer Placeholder</span>
|
required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Sidebar: Tool Interactions */}
|
<div class="mb-3 form-check">
|
||||||
<div className="w-72 bg-gray-50 p-4 border-l overflow-y-auto">
|
<input type="checkbox" class="form-check-input" id="includeMetadata" name="includeMetadata">
|
||||||
<h2 className="text-lg font-semibold mb-4">Tool Panel</h2>
|
<label class="form-check-label" for="includeMetadata"
|
||||||
<div className="space-y-3">
|
th:text="#{splitByChapters.includeMetadata}"></label>
|
||||||
{selectedTool?.id === "split-pdf" ? (
|
<input type="hidden" name="includeMetadata" value="false" />
|
||||||
<SplitPdfPanel />
|
</div>
|
||||||
) : selectedTool ? (
|
|
||||||
<div className="p-2 border rounded bg-white shadow-sm">
|
<div class="mb-3 form-check">
|
||||||
<h3 className="font-semibold text-sm mb-2">{selectedTool.name}</h3>
|
<input type="checkbox" class="form-check-input" id="allowDuplicates" name="allowDuplicates">
|
||||||
<p className="text-xs text-gray-600">This is the panel for {selectedTool.name}.</p>
|
<label class="form-check-label" for="allowDuplicates"
|
||||||
</div>
|
th:text="#{splitByChapters.allowDuplicates}"></label>
|
||||||
) : (
|
<input type="hidden" name="allowDuplicates" value="false" />
|
||||||
<div className="p-2 border rounded bg-white shadow-sm">
|
</div>
|
||||||
<p className="text-sm">Select a tool to begin interacting with the PDF.</p>
|
|
||||||
</div>
|
<p>
|
||||||
)}
|
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
|
||||||
|
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
|
||||||
|
</p>
|
||||||
|
<div class="collapse" id="info">
|
||||||
|
<p th:text="#{splitByChapters.desc.1}"></p>
|
||||||
|
<p th:text="#{splitByChapters.desc.2}"></p>
|
||||||
|
<p th:text="#{splitByChapters.desc.3}"></p>
|
||||||
|
<p th:text="#{splitByChapters.desc.4}"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{splitByChapters.submit}"></button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
}
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
x
Reference in New Issue
Block a user