mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-25 08:59:31 +02:00
Merge pull request #102 from rohit267/feat/pdf-merge
[Feature] Merge PDFs
This commit is contained in:
282
.idea/workspace.xml
generated
282
.idea/workspace.xml
generated
@@ -4,13 +4,9 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="ci: on tag push, push docker images">
|
||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="fix: misc">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/components/input/ToolVideoInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/ToolVideoInput.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/compress/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/compress/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/loop/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/loop/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/rotate/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/rotate/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/trim/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/trim/index.tsx" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -27,7 +23,7 @@
|
||||
<option name="PUSH_AUTO_UPDATE" value="true" />
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="chesterkxng" />
|
||||
<entry key="$PROJECT_DIR$" value="main" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
@@ -46,94 +42,101 @@
|
||||
"state": "OPEN"
|
||||
}
|
||||
}</component>
|
||||
<component name="GitHubPullRequestState">{
|
||||
"prStates": [
|
||||
<component name="GitHubPullRequestState"><![CDATA[{
|
||||
"prStates": [
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts51PkS9",
|
||||
"number": 22
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts51PkS9",
|
||||
"number": 22
|
||||
},
|
||||
"lastSeen": 1741207144695
|
||||
"lastSeen": 1741207144695
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6NiNYl",
|
||||
"number": 32
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6NiNYl",
|
||||
"number": 32
|
||||
},
|
||||
"lastSeen": 1741209723869
|
||||
"lastSeen": 1741209723869
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Nheyd",
|
||||
"number": 31
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Nheyd",
|
||||
"number": 31
|
||||
},
|
||||
"lastSeen": 1741213371410
|
||||
"lastSeen": 1741213371410
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6NmRBs",
|
||||
"number": 33
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6NmRBs",
|
||||
"number": 33
|
||||
},
|
||||
"lastSeen": 1741282429036
|
||||
"lastSeen": 1741282429036
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts5zyFTs",
|
||||
"number": 15
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts5zyFTs",
|
||||
"number": 15
|
||||
},
|
||||
"lastSeen": 1741535540953
|
||||
"lastSeen": 1741535540953
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QQB3c",
|
||||
"number": 59
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QQB3c",
|
||||
"number": 59
|
||||
},
|
||||
"lastSeen": 1743018960900
|
||||
"lastSeen": 1743018960900
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QMPEg",
|
||||
"number": 58
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QMPEg",
|
||||
"number": 58
|
||||
},
|
||||
"lastSeen": 1743019452983
|
||||
"lastSeen": 1743019452983
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QZvRI",
|
||||
"number": 61
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QZvRI",
|
||||
"number": 61
|
||||
},
|
||||
"lastSeen": 1743103196866
|
||||
"lastSeen": 1743103196866
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QqPrQ",
|
||||
"number": 73
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QqPrQ",
|
||||
"number": 73
|
||||
},
|
||||
"lastSeen": 1743265865001
|
||||
"lastSeen": 1743265865001
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Qp5nI",
|
||||
"number": 72
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Qp5nI",
|
||||
"number": 72
|
||||
},
|
||||
"lastSeen": 1743338472110
|
||||
"lastSeen": 1743338472110
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QsjlS",
|
||||
"number": 76
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6QsjlS",
|
||||
"number": 76
|
||||
},
|
||||
"lastSeen": 1743352150953
|
||||
"lastSeen": 1743352150953
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Q0JBe",
|
||||
"number": 82
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6Q0JBe",
|
||||
"number": 82
|
||||
},
|
||||
"lastSeen": 1743470267269
|
||||
"lastSeen": 1743470267269
|
||||
},
|
||||
{
|
||||
"id": {
|
||||
"id": "PR_kwDOMJIfts6UE9-x",
|
||||
"number": 102
|
||||
},
|
||||
"lastSeen": 1747171977348
|
||||
}
|
||||
]
|
||||
}</component>
|
||||
}]]></component>
|
||||
<component name="GithubPullRequestsUISettings">{
|
||||
"selectedUrlAndAccountId": {
|
||||
"url": "https://github.com/iib0011/omni-tools.git",
|
||||
@@ -162,56 +165,56 @@
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"Docker.Dockerfile build.executor": "Run",
|
||||
"Docker.Dockerfile.executor": "Run",
|
||||
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
||||
"Playwright.JoinText Component.executor": "Run",
|
||||
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"Vitest.compute function (1).executor": "Run",
|
||||
"Vitest.compute function.executor": "Run",
|
||||
"Vitest.mergeText.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
||||
"Vitest.parsePageRanges.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
|
||||
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||
"Vitest.replaceText function.executor": "Run",
|
||||
"Vitest.timeBetweenDates.executor": "Run",
|
||||
"git-widget-placeholder": "main",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.dev.executor": "Run",
|
||||
"npm.lint.executor": "Run",
|
||||
"npm.prebuild.executor": "Run",
|
||||
"npm.script:create:tool.executor": "Run",
|
||||
"npm.test.executor": "Run",
|
||||
"npm.test:e2e.executor": "Run",
|
||||
"npm.test:e2e:run.executor": "Run",
|
||||
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
||||
"project.structure.last.edited": "Problems",
|
||||
"project.structure.proportion": "0.0",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "refactai_advanced_settings",
|
||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"Docker.Dockerfile build.executor": "Run",
|
||||
"Docker.Dockerfile.executor": "Run",
|
||||
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
||||
"Playwright.JoinText Component.executor": "Run",
|
||||
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"Vitest.compute function (1).executor": "Run",
|
||||
"Vitest.compute function.executor": "Run",
|
||||
"Vitest.mergeText.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
|
||||
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
||||
"Vitest.parsePageRanges.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
|
||||
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
|
||||
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||
"Vitest.replaceText function.executor": "Run",
|
||||
"Vitest.timeBetweenDates.executor": "Run",
|
||||
"git-widget-placeholder": "#102 on fork/rohit267/feat/pdf-merge",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"npm.build.executor": "Run",
|
||||
"npm.dev.executor": "Run",
|
||||
"npm.lint.executor": "Run",
|
||||
"npm.prebuild.executor": "Run",
|
||||
"npm.script:create:tool.executor": "Run",
|
||||
"npm.test.executor": "Run",
|
||||
"npm.test:e2e.executor": "Run",
|
||||
"npm.test:e2e:run.executor": "Run",
|
||||
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
||||
"project.structure.last.edited": "Problems",
|
||||
"project.structure.proportion": "0.0",
|
||||
"project.structure.side.proportion": "0.2",
|
||||
"settings.editor.selected.configurable": "refactai_advanced_settings",
|
||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
}]]></component>
|
||||
<component name="ReactDesignerToolWindowState">
|
||||
<option name="myId2Visible">
|
||||
<map>
|
||||
@@ -416,31 +419,10 @@
|
||||
<workItem from="1743782726563" duration="2444000" />
|
||||
<workItem from="1743811558991" duration="1279000" />
|
||||
<workItem from="1745523972292" duration="3000" />
|
||||
<workItem from="1745687713234" duration="591000" />
|
||||
</task>
|
||||
<task id="LOCAL-00144" summary="feat: stringify json">
|
||||
<option name="closed" value="true" />
|
||||
<created>1741417920442</created>
|
||||
<option name="number" value="00144" />
|
||||
<option name="presentableId" value="LOCAL-00144" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1741417920442</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00145" summary="feat: arithmetic sequence">
|
||||
<option name="closed" value="true" />
|
||||
<created>1741419142510</created>
|
||||
<option name="number" value="00145" />
|
||||
<option name="presentableId" value="LOCAL-00145" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1741419142510</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00146" summary="style: tools height">
|
||||
<option name="closed" value="true" />
|
||||
<created>1741419188990</created>
|
||||
<option name="number" value="00146" />
|
||||
<option name="presentableId" value="LOCAL-00146" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1741419188990</updated>
|
||||
<workItem from="1745687713234" duration="1747000" />
|
||||
<workItem from="1745775228478" duration="1221000" />
|
||||
<workItem from="1745835676024" duration="68000" />
|
||||
<workItem from="1747171958176" duration="1105000" />
|
||||
</task>
|
||||
<task id="LOCAL-00147" summary="chore: update meta">
|
||||
<option name="closed" value="true" />
|
||||
@@ -810,7 +792,31 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1743811980098</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="193" />
|
||||
<task id="LOCAL-00193" summary="fix: add mkv to supported videos">
|
||||
<option name="closed" value="true" />
|
||||
<created>1745688369521</created>
|
||||
<option name="number" value="00193" />
|
||||
<option name="presentableId" value="LOCAL-00193" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1745688369521</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00194" summary="feat: drag and drop">
|
||||
<option name="closed" value="true" />
|
||||
<created>1745688866294</created>
|
||||
<option name="number" value="00194" />
|
||||
<option name="presentableId" value="LOCAL-00194" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1745688866294</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00195" summary="fix: misc">
|
||||
<option name="closed" value="true" />
|
||||
<created>1747172914927</created>
|
||||
<option name="number" value="00195" />
|
||||
<option name="presentableId" value="LOCAL-00195" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1747172914927</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="196" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -857,10 +863,6 @@
|
||||
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
||||
<option name="CHECK_NEW_TODO" value="false" />
|
||||
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
||||
<MESSAGE value="chore: result file name" />
|
||||
<MESSAGE value="chore: text result extensions" />
|
||||
<MESSAGE value="chore: show new tools in landing" />
|
||||
<MESSAGE value="chore: zoom on hover" />
|
||||
<MESSAGE value="refactor: time between dates" />
|
||||
<MESSAGE value="fix: typos" />
|
||||
<MESSAGE value="feat: compress video" />
|
||||
@@ -882,7 +884,11 @@
|
||||
<MESSAGE value="feat: image to text" />
|
||||
<MESSAGE value="chore: hideCopy if video or audio" />
|
||||
<MESSAGE value="chore: readme img and fix broken link" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="chore: readme img and fix broken link" />
|
||||
<MESSAGE value="fix: add mkv to supported videos" />
|
||||
<MESSAGE value="feat: drag and drop" />
|
||||
<MESSAGE value="Merge branch 'feat/pdf-merge' of git-rohit:rohit267/omni-tools into feat/pdf-merge" />
|
||||
<MESSAGE value="fix: misc" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="fix: misc" />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
|
6313
pnpm-lock.yaml
generated
6313
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import {
|
||||
import { globalInputHeight } from '../../config/uiConfig';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import greyPattern from '@assets/grey-pattern.png';
|
||||
import { isArray } from 'lodash';
|
||||
|
||||
interface BaseFileInputComponentProps extends BaseFileInputProps {
|
||||
children: (props: { preview: string | undefined }) => ReactNode;
|
||||
@@ -32,13 +33,17 @@ export default function BaseFileInput({
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
const objectUrl = createObjectURL(value);
|
||||
setPreview(objectUrl);
|
||||
try {
|
||||
if (isArray(value)) {
|
||||
const objectUrl = createObjectURL(value[0]);
|
||||
setPreview(objectUrl);
|
||||
|
||||
return () => revokeObjectURL(objectUrl);
|
||||
} else {
|
||||
setPreview(null);
|
||||
return () => revokeObjectURL(objectUrl);
|
||||
} else {
|
||||
setPreview(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error previewing file:', error);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
@@ -52,9 +57,9 @@ export default function BaseFileInput({
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (value) {
|
||||
const blob = new Blob([value], { type: value.type });
|
||||
const clipboardItem = new ClipboardItem({ [value.type]: blob });
|
||||
if (isArray(value)) {
|
||||
const blob = new Blob([value[0]], { type: value[0].type });
|
||||
const clipboardItem = new ClipboardItem({ [value[0].type]: blob });
|
||||
|
||||
navigator.clipboard
|
||||
.write([clipboardItem])
|
||||
@@ -65,6 +70,11 @@ export default function BaseFileInput({
|
||||
}
|
||||
};
|
||||
|
||||
function handleClear() {
|
||||
// @ts-ignore
|
||||
onChange(null);
|
||||
}
|
||||
|
||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -206,13 +216,18 @@ export default function BaseFileInput({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
|
||||
<InputFooter
|
||||
handleCopy={handleCopy}
|
||||
handleImport={handleImportClick}
|
||||
handleClear={handleClear}
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
accept={accept.join(',')}
|
||||
onChange={handleFileChange}
|
||||
multiple={false}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@@ -2,23 +2,32 @@ import { Stack } from '@mui/material';
|
||||
import Button from '@mui/material/Button';
|
||||
import PublishIcon from '@mui/icons-material/Publish';
|
||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||
import React from 'react';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
|
||||
export default function InputFooter({
|
||||
handleImport,
|
||||
handleCopy
|
||||
handleCopy,
|
||||
handleClear
|
||||
}: {
|
||||
handleImport: () => void;
|
||||
handleCopy: () => void;
|
||||
handleCopy?: () => void;
|
||||
handleClear?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Stack mt={1} direction={'row'} spacing={2}>
|
||||
<Button onClick={handleImport} startIcon={<PublishIcon />}>
|
||||
Import from file
|
||||
</Button>
|
||||
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
{handleCopy && (
|
||||
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
)}
|
||||
{handleClear && (
|
||||
<Button onClick={handleClear} startIcon={<ClearIcon />}>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
176
src/components/input/ToolMultiplePdfInput.tsx
Normal file
176
src/components/input/ToolMultiplePdfInput.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import { ReactNode, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Box, useTheme } from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import InputHeader from '../InputHeader';
|
||||
import InputFooter from './InputFooter';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import { isArray } from 'lodash';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
|
||||
interface MultiPdfInputComponentProps {
|
||||
accept: string[];
|
||||
title?: string;
|
||||
type: 'pdf';
|
||||
value: MultiPdfInput[];
|
||||
onChange: (file: MultiPdfInput[]) => void;
|
||||
}
|
||||
|
||||
export interface MultiPdfInput {
|
||||
file: File;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export default function ToolMultiFileInput({
|
||||
value,
|
||||
onChange,
|
||||
accept,
|
||||
title,
|
||||
type
|
||||
}: MultiPdfInputComponentProps) {
|
||||
const theme = useTheme();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (files)
|
||||
onChange([
|
||||
...value,
|
||||
...Array.from(files).map((file) => ({ file, order: value.length }))
|
||||
]);
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
function handleClear() {
|
||||
onChange([]);
|
||||
}
|
||||
|
||||
function fileNameTruncate(fileName: string) {
|
||||
const maxLength = 10;
|
||||
if (fileName.length > maxLength) {
|
||||
return fileName.slice(0, maxLength) + '...';
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const sortList = () => {
|
||||
const list = [...value];
|
||||
list.sort((a, b) => a.order - b.order);
|
||||
onChange(list);
|
||||
};
|
||||
|
||||
const reorderList = (sourceIndex: number, destinationIndex: number) => {
|
||||
console.log(sourceIndex, destinationIndex);
|
||||
if (destinationIndex === sourceIndex) {
|
||||
return;
|
||||
}
|
||||
const list = [...value];
|
||||
|
||||
if (destinationIndex === 0) {
|
||||
list[sourceIndex].order = list[0].order - 1;
|
||||
sortList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (destinationIndex === list.length - 1) {
|
||||
list[sourceIndex].order = list[list.length - 1].order + 1;
|
||||
sortList();
|
||||
return;
|
||||
}
|
||||
|
||||
if (destinationIndex < sourceIndex) {
|
||||
list[sourceIndex].order =
|
||||
(list[destinationIndex].order + list[destinationIndex - 1].order) / 2;
|
||||
sortList();
|
||||
return;
|
||||
}
|
||||
|
||||
list[sourceIndex].order =
|
||||
(list[destinationIndex].order + list[destinationIndex + 1].order) / 2;
|
||||
sortList();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader
|
||||
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '300px',
|
||||
|
||||
border: value?.length ? 0 : 1,
|
||||
borderRadius: 2,
|
||||
boxShadow: '5',
|
||||
bgcolor: 'background.paper',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
sx={{
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexWrap: 'wrap',
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{value?.length ? (
|
||||
value.map((file, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{
|
||||
margin: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '200px',
|
||||
border: 1,
|
||||
borderRadius: 1,
|
||||
padding: 1
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<PictureAsPdfIcon />
|
||||
<Typography sx={{ marginLeft: 1 }}>
|
||||
{fileNameTruncate(file.file.name)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
const updatedFiles = value.filter((_, i) => i !== index);
|
||||
onChange(updatedFiles);
|
||||
}}
|
||||
>
|
||||
✖
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No files selected
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<InputFooter handleImport={handleImportClick} handleClear={handleClear} />
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
accept={accept.join(',')}
|
||||
onChange={handleFileChange}
|
||||
multiple={true}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -173,7 +173,9 @@ export default function ToolFileResult({
|
||||
disabled={!value}
|
||||
handleCopy={handleCopy}
|
||||
handleDownload={handleDownload}
|
||||
hideCopy={fileType === 'video' || fileType === 'audio'}
|
||||
hideCopy={
|
||||
fileType === 'video' || fileType === 'audio' || fileType === 'pdf'
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import { tool as pdfRotatePdf } from './rotate-pdf/meta';
|
||||
import { meta as splitPdfMeta } from './split-pdf/meta';
|
||||
import { meta as mergePdf } from './merge-pdf/meta';
|
||||
import { DefinedTool } from '@tools/defineTool';
|
||||
import { tool as compressPdfTool } from './compress-pdf/meta';
|
||||
import { tool as protectPdfTool } from './protect-pdf/meta';
|
||||
import { DefinedTool } from '@tools/defineTool';
|
||||
|
||||
export const pdfTools: DefinedTool[] = [
|
||||
splitPdfMeta,
|
||||
pdfRotatePdf,
|
||||
compressPdfTool,
|
||||
protectPdfTool
|
||||
protectPdfTool,
|
||||
mergePdf
|
||||
];
|
||||
|
66
src/pages/tools/pdf/merge-pdf/index.tsx
Normal file
66
src/pages/tools/pdf/merge-pdf/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState } from 'react';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { mergePdf } from './service';
|
||||
import ToolMultiPdfInput, {
|
||||
MultiPdfInput
|
||||
} from '@components/input/ToolMultiplePdfInput';
|
||||
|
||||
export default function MergePdf({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<MultiPdfInput[]>([]);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
|
||||
const compute = async (values: File[], input: MultiPdfInput[]) => {
|
||||
if (input.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsProcessing(true);
|
||||
const mergeResult = await mergePdf(input.map((i) => i.file));
|
||||
setResult(mergeResult);
|
||||
} catch (error) {
|
||||
throw new Error('Error merging PDF:' + error);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
initialValues={input.map((i) => i.file)}
|
||||
compute={compute}
|
||||
inputComponent={
|
||||
<ToolMultiPdfInput
|
||||
value={input}
|
||||
onChange={(pdfInputs) => {
|
||||
setInput(pdfInputs);
|
||||
}}
|
||||
accept={['application/pdf']}
|
||||
title={'Input PDF'}
|
||||
type="pdf"
|
||||
/>
|
||||
}
|
||||
getGroups={null}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Output merged PDF'}
|
||||
value={result}
|
||||
extension={'pdf'}
|
||||
loading={isProcessing}
|
||||
loadingText={'Extracting pages'}
|
||||
/>
|
||||
}
|
||||
toolInfo={{
|
||||
title: 'How to Use the Merge PDF Tool?',
|
||||
description: `This tool allows you to merge multiple PDF files into a single document.
|
||||
To use the tool, simply upload the PDF files you want to merge. The tool will then combine all pages from the input files into a single PDF document.`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
12
src/pages/tools/pdf/merge-pdf/meta.ts
Normal file
12
src/pages/tools/pdf/merge-pdf/meta.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const meta = defineTool('pdf', {
|
||||
name: 'Merge PDF',
|
||||
shortDescription: 'Merge multiple PDF files into a single document',
|
||||
description: 'Combine multiple PDF files into a single document.',
|
||||
icon: 'material-symbols-light:merge',
|
||||
component: lazy(() => import('./index')),
|
||||
keywords: ['pdf', 'merge', 'extract', 'pages', 'combine', 'document'],
|
||||
path: 'merge-pdf'
|
||||
});
|
43
src/pages/tools/pdf/merge-pdf/service.test.ts
Normal file
43
src/pages/tools/pdf/merge-pdf/service.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { parsePageRanges } from './service';
|
||||
|
||||
describe('parsePageRanges', () => {
|
||||
test('should return all pages when input is empty', () => {
|
||||
expect(parsePageRanges('', 5)).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test('should parse single page numbers', () => {
|
||||
expect(parsePageRanges('1,3,5', 5)).toEqual([1, 3, 5]);
|
||||
});
|
||||
|
||||
test('should parse page ranges', () => {
|
||||
expect(parsePageRanges('2-4', 5)).toEqual([2, 3, 4]);
|
||||
});
|
||||
|
||||
test('should parse mixed page numbers and ranges', () => {
|
||||
expect(parsePageRanges('1,3-5', 5)).toEqual([1, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test('should handle whitespace', () => {
|
||||
expect(parsePageRanges(' 1, 3 - 5 ', 5)).toEqual([1, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test('should ignore invalid page numbers', () => {
|
||||
expect(parsePageRanges('1,a,3', 5)).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
test('should ignore out-of-range page numbers', () => {
|
||||
expect(parsePageRanges('1,6,3', 5)).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
test('should limit ranges to valid pages', () => {
|
||||
expect(parsePageRanges('0-6', 5)).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test('should handle reversed ranges', () => {
|
||||
expect(parsePageRanges('4-2', 5)).toEqual([2, 3, 4]);
|
||||
});
|
||||
|
||||
test('should remove duplicates', () => {
|
||||
expect(parsePageRanges('1,1,2,2-4,3', 5)).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
});
|
95
src/pages/tools/pdf/merge-pdf/service.ts
Normal file
95
src/pages/tools/pdf/merge-pdf/service.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
/**
|
||||
* Parses a page range string and returns an array of page numbers
|
||||
* @param pageRangeStr String like "1,3-5,7" to extract pages 1, 3, 4, 5, and 7
|
||||
* @param totalPages Total number of pages in the PDF
|
||||
* @returns Array of page numbers to extract
|
||||
*/
|
||||
export function parsePageRanges(
|
||||
pageRangeStr: string,
|
||||
totalPages: number
|
||||
): number[] {
|
||||
if (!pageRangeStr.trim()) {
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
const pageNumbers = new Set<number>();
|
||||
const ranges = pageRangeStr.split(',');
|
||||
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (!isNaN(start) && !isNaN(end)) {
|
||||
// Handle both forward and reversed ranges
|
||||
const normalizedStart = Math.min(start, end);
|
||||
const normalizedEnd = Math.max(start, end);
|
||||
|
||||
for (
|
||||
let i = Math.max(1, normalizedStart);
|
||||
i <= Math.min(totalPages, normalizedEnd);
|
||||
i++
|
||||
) {
|
||||
pageNumbers.add(i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const pageNum = parseInt(trimmedRange, 10);
|
||||
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
|
||||
pageNumbers.add(pageNum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...pageNumbers].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a PDF file based on specified page ranges
|
||||
* @param pdfFile The input PDF file
|
||||
* @param pageRanges String specifying which pages to extract (e.g., "1,3-5,7")
|
||||
* @returns Promise resolving to a new PDF file with only the selected pages
|
||||
*/
|
||||
export async function splitPdf(
|
||||
pdfFile: File,
|
||||
pageRanges: string
|
||||
): Promise<File> {
|
||||
const arrayBuffer = await pdfFile.arrayBuffer();
|
||||
const sourcePdf = await PDFDocument.load(arrayBuffer);
|
||||
const totalPages = sourcePdf.getPageCount();
|
||||
const pagesToExtract = parsePageRanges(pageRanges, totalPages);
|
||||
|
||||
const newPdf = await PDFDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(
|
||||
sourcePdf,
|
||||
pagesToExtract.map((pageNum) => pageNum - 1)
|
||||
);
|
||||
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const newFileName = pdfFile.name.replace('.pdf', '-extracted.pdf');
|
||||
return new File([newPdfBytes], newFileName, { type: 'application/pdf' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges multiple PDF files into a single document
|
||||
* @param pdfFiles Array of PDF files to merge
|
||||
* @returns Promise resolving to a new PDF file with all pages combined
|
||||
*/
|
||||
export async function mergePdf(pdfFiles: File[]): Promise<File> {
|
||||
const mergedPdf = await PDFDocument.create();
|
||||
for (const pdfFile of pdfFiles) {
|
||||
const arrayBuffer = await pdfFile.arrayBuffer();
|
||||
const pdf = await PDFDocument.load(arrayBuffer);
|
||||
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
||||
copiedPages.forEach((page) => mergedPdf.addPage(page));
|
||||
}
|
||||
|
||||
const mergedPdfBytes = await mergedPdf.save();
|
||||
const mergedFileName = 'merged.pdf';
|
||||
return new File([mergedPdfBytes], mergedFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
@@ -10,6 +10,7 @@ import { InitialValuesType, RotationAngle } from './types';
|
||||
import { parsePageRanges, rotatePdf } from './service';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { isArray } from 'lodash';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
rotationAngle: 90,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
|
Reference in New Issue
Block a user