mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Merge commit 'cd2b82d614b57b6024a6c2ee1f8602ea7f2634b2' into feature/v2/automate
This commit is contained in:
commit
441b0d837e
@ -1011,7 +1011,49 @@
|
|||||||
"submit": "Change"
|
"submit": "Change"
|
||||||
},
|
},
|
||||||
"removePages": {
|
"removePages": {
|
||||||
"tags": "Remove pages,delete pages"
|
"tags": "Remove pages,delete pages",
|
||||||
|
"title": "Remove Pages",
|
||||||
|
"pageNumbers": "Pages to Remove",
|
||||||
|
"pageNumbersPlaceholder": "e.g. 1,3,5-7",
|
||||||
|
"pageNumbersHelp": "Enter page numbers separated by commas, or ranges like 1-5. Example: 1,3,5-7",
|
||||||
|
"filenamePrefix": "pages_removed",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Page Selection"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred whilst removing pages."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Page Removal Results"
|
||||||
|
},
|
||||||
|
"submit": "Remove Pages"
|
||||||
|
},
|
||||||
|
"pageSelection": {
|
||||||
|
"tooltip": {
|
||||||
|
"header": {
|
||||||
|
"title": "Page Selection Guide"
|
||||||
|
},
|
||||||
|
"basic": {
|
||||||
|
"title": "Basic Usage",
|
||||||
|
"text": "Select specific pages from your PDF document using simple syntax.",
|
||||||
|
"bullet1": "Individual pages: 1,3,5",
|
||||||
|
"bullet2": "Page ranges: 3-6 or 10-15",
|
||||||
|
"bullet3": "All pages: all"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Advanced Features"
|
||||||
|
},
|
||||||
|
"tips": {
|
||||||
|
"title": "Tips",
|
||||||
|
"text": "Keep these guidelines in mind:",
|
||||||
|
"bullet1": "Page numbers start from 1 (not 0)",
|
||||||
|
"bullet2": "Spaces are automatically removed",
|
||||||
|
"bullet3": "Invalid expressions are ignored"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"compressPdfs": {
|
"compressPdfs": {
|
||||||
"tags": "squish,small,tiny"
|
"tags": "squish,small,tiny"
|
||||||
@ -1020,7 +1062,18 @@
|
|||||||
"tags": "remove,delete,form,field,readonly",
|
"tags": "remove,delete,form,field,readonly",
|
||||||
"title": "Remove Read-Only from Form Fields",
|
"title": "Remove Read-Only from Form Fields",
|
||||||
"header": "Unlock PDF Forms",
|
"header": "Unlock PDF Forms",
|
||||||
"submit": "Remove"
|
"submit": "Unlock Forms",
|
||||||
|
"description": "This tool will remove read-only restrictions from PDF form fields, making them editable and fillable.",
|
||||||
|
"filenamePrefix": "unlocked_forms",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred whilst unlocking PDF forms."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Unlocked Forms Results"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"changeMetadata": {
|
"changeMetadata": {
|
||||||
"tags": "Title,author,date,creation,time,publisher,producer,stats",
|
"tags": "Title,author,date,creation,time,publisher,producer,stats",
|
||||||
@ -1188,7 +1241,18 @@
|
|||||||
"tags": "fix,restore,correction,recover",
|
"tags": "fix,restore,correction,recover",
|
||||||
"title": "Repair",
|
"title": "Repair",
|
||||||
"header": "Repair PDFs",
|
"header": "Repair PDFs",
|
||||||
"submit": "Repair"
|
"submit": "Repair",
|
||||||
|
"description": "This tool will attempt to repair corrupted or damaged PDF files. No additional settings are required.",
|
||||||
|
"filenamePrefix": "repaired",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred whilst repairing the PDF."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Repair Results"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"removeBlanks": {
|
"removeBlanks": {
|
||||||
"tags": "cleanup,streamline,non-content,organize",
|
"tags": "cleanup,streamline,non-content,organize",
|
||||||
@ -1257,7 +1321,18 @@
|
|||||||
"title": "Remove Certificate Signature",
|
"title": "Remove Certificate Signature",
|
||||||
"header": "Remove the digital certificate from the PDF",
|
"header": "Remove the digital certificate from the PDF",
|
||||||
"selectPDF": "Select a PDF file:",
|
"selectPDF": "Select a PDF file:",
|
||||||
"submit": "Remove Signature"
|
"submit": "Remove Signature",
|
||||||
|
"description": "This tool will remove digital certificate signatures from your PDF document.",
|
||||||
|
"filenamePrefix": "unsigned",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred whilst removing certificate signatures."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Certificate Removal Results"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"pageLayout": {
|
"pageLayout": {
|
||||||
"tags": "merge,composite,single-view,organize",
|
"tags": "merge,composite,single-view,organize",
|
||||||
@ -1585,7 +1660,18 @@
|
|||||||
"pdfToSinglePage": {
|
"pdfToSinglePage": {
|
||||||
"title": "PDF To Single Page",
|
"title": "PDF To Single Page",
|
||||||
"header": "PDF To Single Page",
|
"header": "PDF To Single Page",
|
||||||
"submit": "Convert To Single Page"
|
"submit": "Convert To Single Page",
|
||||||
|
"description": "This tool will merge all pages of your PDF into one large single page. The width will remain the same as the original pages, but the height will be the sum of all page heights.",
|
||||||
|
"filenamePrefix": "single_page",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred whilst converting to single page."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Single Page Results"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"pageExtracter": {
|
"pageExtracter": {
|
||||||
"title": "Extract Pages",
|
"title": "Extract Pages",
|
||||||
@ -1945,7 +2031,14 @@
|
|||||||
"fileSize": "Size",
|
"fileSize": "Size",
|
||||||
"fileVersion": "Version",
|
"fileVersion": "Version",
|
||||||
"totalSelected": "Total Selected",
|
"totalSelected": "Total Selected",
|
||||||
"dropFilesHere": "Drop files here"
|
"dropFilesHere": "Drop files here",
|
||||||
|
"selectAll": "Select All",
|
||||||
|
"deselectAll": "Deselect All",
|
||||||
|
"deleteSelected": "Delete Selected",
|
||||||
|
"downloadSelected": "Download Selected",
|
||||||
|
"selectedCount": "{{count}} selected",
|
||||||
|
"download": "Download",
|
||||||
|
"delete": "Delete"
|
||||||
},
|
},
|
||||||
"storage": {
|
"storage": {
|
||||||
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
|
"temporaryNotice": "Files are stored temporarily in your browser and may be cleared automatically",
|
||||||
|
@ -738,7 +738,72 @@
|
|||||||
"submit": "Change"
|
"submit": "Change"
|
||||||
},
|
},
|
||||||
"removePages": {
|
"removePages": {
|
||||||
"tags": "Remove pages,delete pages"
|
"tags": "Remove pages,delete pages",
|
||||||
|
"title": "Remove Pages",
|
||||||
|
"pageNumbers": "Pages to Remove",
|
||||||
|
"pageNumbersPlaceholder": "e.g. 1,3,5-7",
|
||||||
|
"pageNumbersHelp": "Enter page numbers separated by commas, or ranges like 1-5. Example: 1,3,5-7",
|
||||||
|
"filenamePrefix": "pages_removed",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Page Selection"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while removing pages."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Page Removal Results"
|
||||||
|
},
|
||||||
|
"submit": "Remove Pages"
|
||||||
|
},
|
||||||
|
"pageSelection": {
|
||||||
|
"tooltip": {
|
||||||
|
"header": {
|
||||||
|
"title": "Page Selection Guide"
|
||||||
|
},
|
||||||
|
"basic": {
|
||||||
|
"title": "Basic Usage",
|
||||||
|
"text": "Select specific pages from your PDF document using simple syntax.",
|
||||||
|
"bullet1": "Individual pages: 1,3,5",
|
||||||
|
"bullet2": "Page ranges: 3-6 or 10-15",
|
||||||
|
"bullet3": "All pages: all"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"title": "Advanced Features",
|
||||||
|
"expandText": "▶ Show advanced options",
|
||||||
|
"collapseText": "▼ Hide advanced options",
|
||||||
|
"mathematical": {
|
||||||
|
"title": "Mathematical Functions",
|
||||||
|
"text": "Use mathematical expressions to select page patterns:",
|
||||||
|
"bullet1": "2n - all even pages (2, 4, 6, 8...)",
|
||||||
|
"bullet2": "2n+1 - all odd pages (1, 3, 5, 7...)",
|
||||||
|
"bullet3": "3n - every 3rd page (3, 6, 9, 12...)",
|
||||||
|
"bullet4": "4n-1 - pages 3, 7, 11, 15..."
|
||||||
|
},
|
||||||
|
"ranges": {
|
||||||
|
"title": "Open-ended Ranges",
|
||||||
|
"text": "Select from a starting point to the end:",
|
||||||
|
"bullet1": "5- selects pages 5 to end of document",
|
||||||
|
"bullet2": "10- selects pages 10 to end"
|
||||||
|
},
|
||||||
|
"combinations": {
|
||||||
|
"title": "Complex Combinations",
|
||||||
|
"text": "Combine different selection methods:",
|
||||||
|
"bullet1": "1,3-5,8,2n - pages 1, 3-5, 8, and all even pages",
|
||||||
|
"bullet2": "10-,2n+1 - pages 10 to end plus all odd pages",
|
||||||
|
"bullet3": "1-5,15-,3n - pages 1-5, 15 to end, and every 3rd page"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tips": {
|
||||||
|
"title": "Tips",
|
||||||
|
"text": "Keep these guidelines in mind:",
|
||||||
|
"bullet1": "Page numbers start from 1 (not 0)",
|
||||||
|
"bullet2": "Spaces are automatically removed",
|
||||||
|
"bullet3": "Invalid expressions are ignored"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"compressPdfs": {
|
"compressPdfs": {
|
||||||
"tags": "squish,small,tiny"
|
"tags": "squish,small,tiny"
|
||||||
@ -747,7 +812,18 @@
|
|||||||
"tags": "remove,delete,form,field,readonly",
|
"tags": "remove,delete,form,field,readonly",
|
||||||
"title": "Remove Read-Only from Form Fields",
|
"title": "Remove Read-Only from Form Fields",
|
||||||
"header": "Unlock PDF Forms",
|
"header": "Unlock PDF Forms",
|
||||||
"submit": "Remove"
|
"submit": "Unlock Forms",
|
||||||
|
"description": "This tool will remove read-only restrictions from PDF form fields, making them editable and fillable.",
|
||||||
|
"filenamePrefix": "unlocked_forms",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while unlocking PDF forms."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Unlocked Forms Results"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"changeMetadata": {
|
"changeMetadata": {
|
||||||
"tags": "Title,author,date,creation,time,publisher,producer,stats",
|
"tags": "Title,author,date,creation,time,publisher,producer,stats",
|
||||||
@ -915,7 +991,18 @@
|
|||||||
"tags": "fix,restore,correction,recover",
|
"tags": "fix,restore,correction,recover",
|
||||||
"title": "Repair",
|
"title": "Repair",
|
||||||
"header": "Repair PDFs",
|
"header": "Repair PDFs",
|
||||||
"submit": "Repair"
|
"submit": "Repair",
|
||||||
|
"description": "This tool will attempt to repair corrupted or damaged PDF files. No additional settings are required.",
|
||||||
|
"filenamePrefix": "repaired",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while repairing the PDF."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Repair Results"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"removeBlanks": {
|
"removeBlanks": {
|
||||||
"tags": "cleanup,streamline,non-content,organize",
|
"tags": "cleanup,streamline,non-content,organize",
|
||||||
@ -984,7 +1071,18 @@
|
|||||||
"title": "Remove Certificate Signature",
|
"title": "Remove Certificate Signature",
|
||||||
"header": "Remove the digital certificate from the PDF",
|
"header": "Remove the digital certificate from the PDF",
|
||||||
"selectPDF": "Select a PDF file:",
|
"selectPDF": "Select a PDF file:",
|
||||||
"submit": "Remove Signature"
|
"submit": "Remove Signature",
|
||||||
|
"description": "This tool will remove digital certificate signatures from your PDF document.",
|
||||||
|
"filenamePrefix": "unsigned",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while removing certificate signatures."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Certificate Removal Results"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"pageLayout": {
|
"pageLayout": {
|
||||||
"tags": "merge,composite,single-view,organize",
|
"tags": "merge,composite,single-view,organize",
|
||||||
@ -1312,7 +1410,18 @@
|
|||||||
"pdfToSinglePage": {
|
"pdfToSinglePage": {
|
||||||
"title": "PDF To Single Page",
|
"title": "PDF To Single Page",
|
||||||
"header": "PDF To Single Page",
|
"header": "PDF To Single Page",
|
||||||
"submit": "Convert To Single Page"
|
"submit": "Convert To Single Page",
|
||||||
|
"description": "This tool will merge all pages of your PDF into one large single page. The width will remain the same as the original pages, but the height will be the sum of all page heights.",
|
||||||
|
"filenamePrefix": "single_page",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select a PDF file in the main view to get started"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while converting to single page."
|
||||||
|
},
|
||||||
|
"results": {
|
||||||
|
"title": "Single Page Results"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"pageExtracter": {
|
"pageExtracter": {
|
||||||
"title": "Extract Pages",
|
"title": "Extract Pages",
|
||||||
|
@ -4,6 +4,7 @@ import FileSourceButtons from './FileSourceButtons';
|
|||||||
import FileDetails from './FileDetails';
|
import FileDetails from './FileDetails';
|
||||||
import SearchInput from './SearchInput';
|
import SearchInput from './SearchInput';
|
||||||
import FileListArea from './FileListArea';
|
import FileListArea from './FileListArea';
|
||||||
|
import FileActions from './FileActions';
|
||||||
import HiddenFileInput from './HiddenFileInput';
|
import HiddenFileInput from './HiddenFileInput';
|
||||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||||
|
|
||||||
@ -17,27 +18,27 @@ const DesktopLayout: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Grid gutter="xs" h="100%" grow={false} style={{ flexWrap: 'nowrap', minWidth: 0 }}>
|
<Grid gutter="xs" h="100%" grow={false} style={{ flexWrap: 'nowrap', minWidth: 0 }}>
|
||||||
{/* Column 1: File Sources */}
|
{/* Column 1: File Sources */}
|
||||||
<Grid.Col span="content" p="lg" style={{
|
<Grid.Col span="content" p="lg" style={{
|
||||||
minWidth: '13.625rem',
|
minWidth: '13.625rem',
|
||||||
width: '13.625rem',
|
width: '13.625rem',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
}}>
|
}}>
|
||||||
<FileSourceButtons />
|
<FileSourceButtons />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
{/* Column 2: File List */}
|
{/* Column 2: File List */}
|
||||||
<Grid.Col span="auto" style={{
|
<Grid.Col span="auto" style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
flex: '1 1 0px'
|
flex: '1 1 0px'
|
||||||
}}>
|
}}>
|
||||||
<div style={{
|
<div style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
backgroundColor: 'var(--bg-file-list)',
|
backgroundColor: 'var(--bg-file-list)',
|
||||||
border: '1px solid var(--mantine-color-gray-2)',
|
border: '1px solid var(--mantine-color-gray-2)',
|
||||||
@ -45,18 +46,26 @@ const DesktopLayout: React.FC = () => {
|
|||||||
overflow: 'hidden'
|
overflow: 'hidden'
|
||||||
}}>
|
}}>
|
||||||
{activeSource === 'recent' && (
|
{activeSource === 'recent' && (
|
||||||
<div style={{
|
<>
|
||||||
flexShrink: 0,
|
<div style={{
|
||||||
borderBottom: '1px solid var(--mantine-color-gray-3)'
|
flexShrink: 0,
|
||||||
}}>
|
borderBottom: '1px solid var(--mantine-color-gray-3)'
|
||||||
<SearchInput />
|
}}>
|
||||||
</div>
|
<SearchInput />
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
borderBottom: '1px solid var(--mantine-color-gray-3)'
|
||||||
|
}}>
|
||||||
|
<FileActions />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ flex: 1, minHeight: 0 }}>
|
<div style={{ flex: 1, minHeight: 0 }}>
|
||||||
<FileListArea
|
<FileListArea
|
||||||
scrollAreaHeight={`calc(${modalHeight} )`}
|
scrollAreaHeight={`calc(${modalHeight} )`}
|
||||||
scrollAreaStyle={{
|
scrollAreaStyle={{
|
||||||
height: activeSource === 'recent' && recentFiles.length > 0 ? modalHeight : '100%',
|
height: activeSource === 'recent' && recentFiles.length > 0 ? modalHeight : '100%',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
@ -66,12 +75,12 @@ const DesktopLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
{/* Column 3: File Details */}
|
{/* Column 3: File Details */}
|
||||||
<Grid.Col p="xl" span="content" style={{
|
<Grid.Col p="xl" span="content" style={{
|
||||||
minWidth: '25rem',
|
minWidth: '25rem',
|
||||||
width: '25rem',
|
width: '25rem',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
maxWidth: '18rem'
|
maxWidth: '18rem'
|
||||||
}}>
|
}}>
|
||||||
@ -79,11 +88,11 @@ const DesktopLayout: React.FC = () => {
|
|||||||
<FileDetails />
|
<FileDetails />
|
||||||
</div>
|
</div>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
{/* Hidden file input for local file selection */}
|
{/* Hidden file input for local file selection */}
|
||||||
<HiddenFileInput />
|
<HiddenFileInput />
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DesktopLayout;
|
export default DesktopLayout;
|
||||||
|
115
frontend/src/components/fileManager/FileActions.tsx
Normal file
115
frontend/src/components/fileManager/FileActions.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Group, Text, ActionIcon, Tooltip } from "@mantine/core";
|
||||||
|
import SelectAllIcon from "@mui/icons-material/SelectAll";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useFileManagerContext } from "../../contexts/FileManagerContext";
|
||||||
|
|
||||||
|
const FileActions: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { recentFiles, selectedFileIds, filteredFiles, onSelectAll, onDeleteSelected, onDownloadSelected } =
|
||||||
|
useFileManagerContext();
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
onSelectAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteSelected = () => {
|
||||||
|
if (selectedFileIds.length > 0) {
|
||||||
|
onDeleteSelected();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadSelected = () => {
|
||||||
|
if (selectedFileIds.length > 0) {
|
||||||
|
onDownloadSelected();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only show actions if there are files
|
||||||
|
if (recentFiles.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length;
|
||||||
|
const hasSelection = selectedFileIds.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "0.75rem 1rem",
|
||||||
|
backgroundColor: "var(--mantine-color-gray-1)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
minHeight: "3rem",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Left: Select All */}
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
label={allFilesSelected ? t("fileManager.deselectAll", "Deselect All") : t("fileManager.selectAll", "Select All")}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
color="dimmed"
|
||||||
|
onClick={handleSelectAll}
|
||||||
|
disabled={filteredFiles.length === 0}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
<SelectAllIcon style={{ fontSize: "1rem" }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center: Selected count */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasSelection && (
|
||||||
|
<Text size="sm" c="dimmed" fw={500}>
|
||||||
|
{t("fileManager.selectedCount", "{{count}} selected", { count: selectedFileIds.length })}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Delete and Download */}
|
||||||
|
<Group gap="xs">
|
||||||
|
<Tooltip label={t("fileManager.deleteSelected", "Delete Selected")}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
color="dimmed"
|
||||||
|
onClick={handleDeleteSelected}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
<DeleteIcon style={{ fontSize: "1rem" }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label={t("fileManager.downloadSelected", "Download Selected")}>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
color="dimmed"
|
||||||
|
onClick={handleDownloadSelected}
|
||||||
|
disabled={!hasSelection}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
<DownloadIcon style={{ fontSize: "1rem" }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileActions;
|
@ -19,22 +19,23 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
activeSource,
|
activeSource,
|
||||||
recentFiles,
|
recentFiles,
|
||||||
filteredFiles,
|
filteredFiles,
|
||||||
selectedFileIds,
|
selectedFilesSet,
|
||||||
onFileSelect,
|
onFileSelect,
|
||||||
onFileRemove,
|
onFileRemove,
|
||||||
onFileDoubleClick,
|
onFileDoubleClick,
|
||||||
|
onDownloadSingle,
|
||||||
isFileSupported,
|
isFileSupported,
|
||||||
} = useFileManagerContext();
|
} = useFileManagerContext();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (activeSource === 'recent') {
|
if (activeSource === 'recent') {
|
||||||
return (
|
return (
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
h={scrollAreaHeight}
|
h={scrollAreaHeight}
|
||||||
style={{
|
style={{
|
||||||
...scrollAreaStyle
|
...scrollAreaStyle
|
||||||
}}
|
}}
|
||||||
type="always"
|
type="always"
|
||||||
scrollbarSize={8}
|
scrollbarSize={8}
|
||||||
>
|
>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
@ -53,10 +54,11 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
<FileListItem
|
<FileListItem
|
||||||
key={file.id || file.name}
|
key={file.id || file.name}
|
||||||
file={file}
|
file={file}
|
||||||
isSelected={selectedFileIds.includes(file.id || file.name)}
|
isSelected={selectedFilesSet.has(file.id || file.name)}
|
||||||
isSupported={isFileSupported(file.name)}
|
isSupported={isFileSupported(file.name)}
|
||||||
onSelect={() => onFileSelect(file)}
|
onSelect={(shiftKey) => onFileSelect(file, index, shiftKey)}
|
||||||
onRemove={() => onFileRemove(index)}
|
onRemove={() => onFileRemove(index)}
|
||||||
|
onDownload={() => onDownloadSingle(file)}
|
||||||
onDoubleClick={() => onFileDoubleClick(file)}
|
onDoubleClick={() => onFileDoubleClick(file)}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@ -77,4 +79,4 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FileListArea;
|
export default FileListArea;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core';
|
import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu } from '@mantine/core';
|
||||||
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { getFileSize, getFileDate } from '../../utils/fileUtils';
|
import { getFileSize, getFileDate } from '../../utils/fileUtils';
|
||||||
import { FileWithUrl } from '../../types/file';
|
import { FileWithUrl } from '../../types/file';
|
||||||
|
|
||||||
@ -8,33 +11,44 @@ interface FileListItemProps {
|
|||||||
file: FileWithUrl;
|
file: FileWithUrl;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
isSupported: boolean;
|
isSupported: boolean;
|
||||||
onSelect: () => void;
|
onSelect: (shiftKey?: boolean) => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
|
onDownload?: () => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileListItem: React.FC<FileListItemProps> = ({
|
const FileListItem: React.FC<FileListItemProps> = ({
|
||||||
file,
|
file,
|
||||||
isSelected,
|
isSelected,
|
||||||
isSupported,
|
isSupported,
|
||||||
onSelect,
|
onSelect,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
onDownload,
|
||||||
onDoubleClick
|
onDoubleClick
|
||||||
}) => {
|
}) => {
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// Keep item in hovered state if menu is open
|
||||||
|
const shouldShowHovered = isHovered || isMenuOpen;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
p="sm"
|
p="sm"
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
backgroundColor: isSelected ? 'var(--mantine-color-gray-0)' : (isHovered ? 'var(--mantine-color-gray-0)' : 'var(--bg-file-list)'),
|
backgroundColor: isSelected ? 'var(--mantine-color-gray-1)' : (shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'var(--bg-file-list)'),
|
||||||
opacity: isSupported ? 1 : 0.5,
|
opacity: isSupported ? 1 : 0.5,
|
||||||
transition: 'background-color 0.15s ease'
|
transition: 'background-color 0.15s ease',
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
MozUserSelect: 'none',
|
||||||
|
msUserSelect: 'none'
|
||||||
}}
|
}}
|
||||||
onClick={onSelect}
|
onClick={(e) => onSelect(e.shiftKey)}
|
||||||
onDoubleClick={onDoubleClick}
|
onDoubleClick={onDoubleClick}
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
@ -54,26 +68,59 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text size="sm" fw={500} truncate>{file.name}</Text>
|
<Text size="sm" fw={500} truncate>{file.name}</Text>
|
||||||
<Text size="xs" c="dimmed">{getFileSize(file)} • {getFileDate(file)}</Text>
|
<Text size="xs" c="dimmed">{getFileSize(file)} • {getFileDate(file)}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
{/* Delete button - fades in/out on hover */}
|
|
||||||
<ActionIcon
|
{/* Three dots menu - fades in/out on hover */}
|
||||||
variant="subtle"
|
<Menu
|
||||||
c="dimmed"
|
position="bottom-end"
|
||||||
size="md"
|
withinPortal
|
||||||
onClick={(e) => { e.stopPropagation(); onRemove(); }}
|
onOpen={() => setIsMenuOpen(true)}
|
||||||
style={{
|
onClose={() => setIsMenuOpen(false)}
|
||||||
opacity: isHovered ? 1 : 0,
|
|
||||||
transform: isHovered ? 'scale(1)' : 'scale(0.8)',
|
|
||||||
transition: 'opacity 0.3s ease, transform 0.3s ease',
|
|
||||||
pointerEvents: isHovered ? 'auto' : 'none'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<DeleteIcon style={{ fontSize: 20 }} />
|
<Menu.Target>
|
||||||
</ActionIcon>
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
c="dimmed"
|
||||||
|
size="md"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
opacity: shouldShowHovered ? 1 : 0,
|
||||||
|
transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)',
|
||||||
|
transition: 'opacity 0.3s ease, transform 0.3s ease',
|
||||||
|
pointerEvents: shouldShowHovered ? 'auto' : 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MoreVertIcon style={{ fontSize: 20 }} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
|
||||||
|
<Menu.Dropdown>
|
||||||
|
{onDownload && (
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<DownloadIcon style={{ fontSize: 16 }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDownload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('fileManager.download', 'Download')}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<DeleteIcon style={{ fontSize: 16 }} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('fileManager.delete', 'Delete')}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
{ <Divider color="var(--mantine-color-gray-3)" />}
|
{ <Divider color="var(--mantine-color-gray-3)" />}
|
||||||
@ -81,4 +128,4 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FileListItem;
|
export default FileListItem;
|
||||||
|
@ -4,6 +4,7 @@ import FileSourceButtons from './FileSourceButtons';
|
|||||||
import FileDetails from './FileDetails';
|
import FileDetails from './FileDetails';
|
||||||
import SearchInput from './SearchInput';
|
import SearchInput from './SearchInput';
|
||||||
import FileListArea from './FileListArea';
|
import FileListArea from './FileListArea';
|
||||||
|
import FileActions from './FileActions';
|
||||||
import HiddenFileInput from './HiddenFileInput';
|
import HiddenFileInput from './HiddenFileInput';
|
||||||
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
import { useFileManagerContext } from '../../contexts/FileManagerContext';
|
||||||
|
|
||||||
@ -22,10 +23,11 @@ const MobileLayout: React.FC = () => {
|
|||||||
// Estimate heights of fixed components
|
// Estimate heights of fixed components
|
||||||
const fileSourceHeight = '3rem'; // FileSourceButtons height
|
const fileSourceHeight = '3rem'; // FileSourceButtons height
|
||||||
const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height
|
const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height
|
||||||
|
const fileActionsHeight = activeSource === 'recent' ? '3rem' : '0rem'; // FileActions height (now at bottom)
|
||||||
const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height
|
const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height
|
||||||
const gapHeight = activeSource === 'recent' ? '3rem' : '2rem'; // Stack gaps
|
const gapHeight = activeSource === 'recent' ? '3.75rem' : '2rem'; // Stack gaps
|
||||||
|
|
||||||
return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${searchHeight} - ${gapHeight})`;
|
return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${fileActionsHeight} - ${searchHeight} - ${gapHeight})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -51,12 +53,20 @@ const MobileLayout: React.FC = () => {
|
|||||||
minHeight: 0
|
minHeight: 0
|
||||||
}}>
|
}}>
|
||||||
{activeSource === 'recent' && (
|
{activeSource === 'recent' && (
|
||||||
<Box style={{
|
<>
|
||||||
flexShrink: 0,
|
<Box style={{
|
||||||
borderBottom: '1px solid var(--mantine-color-gray-2)'
|
flexShrink: 0,
|
||||||
}}>
|
borderBottom: '1px solid var(--mantine-color-gray-2)'
|
||||||
<SearchInput />
|
}}>
|
||||||
</Box>
|
<SearchInput />
|
||||||
|
</Box>
|
||||||
|
<Box style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
borderBottom: '1px solid var(--mantine-color-gray-2)'
|
||||||
|
}}>
|
||||||
|
<FileActions />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box style={{ flex: 1, minHeight: 0 }}>
|
<Box style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RemoveCertificateSignParameters } from '../../../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters';
|
||||||
|
|
||||||
|
interface RemoveCertificateSignSettingsProps {
|
||||||
|
parameters: RemoveCertificateSignParameters;
|
||||||
|
onParameterChange: <K extends keyof RemoveCertificateSignParameters>(parameter: K, value: RemoveCertificateSignParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RemoveCertificateSignSettings: React.FC<RemoveCertificateSignSettingsProps> = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange, // Unused - kept for interface consistency and future extensibility
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="remove-certificate-sign-settings">
|
||||||
|
<p className="text-muted">
|
||||||
|
{t('removeCertSign.description', 'This tool will remove digital certificate signatures from your PDF document.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemoveCertificateSignSettings;
|
27
frontend/src/components/tools/repair/RepairSettings.tsx
Normal file
27
frontend/src/components/tools/repair/RepairSettings.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RepairParameters } from '../../../hooks/tools/repair/useRepairParameters';
|
||||||
|
|
||||||
|
interface RepairSettingsProps {
|
||||||
|
parameters: RepairParameters;
|
||||||
|
onParameterChange: <K extends keyof RepairParameters>(parameter: K, value: RepairParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RepairSettings: React.FC<RepairSettingsProps> = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="repair-settings">
|
||||||
|
<p className="text-muted">
|
||||||
|
{t('repair.description', 'This tool will attempt to repair corrupted or damaged PDF files. No additional settings are required.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RepairSettings;
|
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SingleLargePageParameters } from '../../../hooks/tools/singleLargePage/useSingleLargePageParameters';
|
||||||
|
|
||||||
|
interface SingleLargePageSettingsProps {
|
||||||
|
parameters: SingleLargePageParameters;
|
||||||
|
onParameterChange: <K extends keyof SingleLargePageParameters>(parameter: K, value: SingleLargePageParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SingleLargePageSettings: React.FC<SingleLargePageSettingsProps> = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="single-large-page-settings">
|
||||||
|
<p className="text-muted">
|
||||||
|
{t('pdfToSinglePage.description', 'This tool will merge all pages of your PDF into one large single page. The width will remain the same as the original pages, but the height will be the sum of all page heights.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SingleLargePageSettings;
|
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { UnlockPdfFormsParameters } from '../../../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters';
|
||||||
|
|
||||||
|
interface UnlockPdfFormsSettingsProps {
|
||||||
|
parameters: UnlockPdfFormsParameters;
|
||||||
|
onParameterChange: <K extends keyof UnlockPdfFormsParameters>(parameter: K, value: UnlockPdfFormsParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnlockPdfFormsSettings: React.FC<UnlockPdfFormsSettingsProps> = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange, // Unused - kept for interface consistency and future extensibility
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="unlock-pdf-forms-settings">
|
||||||
|
<p className="text-muted">
|
||||||
|
{t('unlockPDFForms.description', 'This tool will remove read-only restrictions from PDF form fields, making them editable and fillable.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnlockPdfFormsSettings;
|
62
frontend/src/components/tooltips/usePageSelectionTips.tsx
Normal file
62
frontend/src/components/tooltips/usePageSelectionTips.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '../../types/tips';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable tooltip for page selection functionality.
|
||||||
|
* Can be used by any tool that uses the GeneralUtils.parsePageList syntax.
|
||||||
|
*/
|
||||||
|
export const usePageSelectionTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("pageSelection.tooltip.header.title", "Page Selection Guide")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
description: t("pageSelection.tooltip.description", "Choose which pages to use for the operation. Supports single pages, ranges, formulas, and the all keyword.")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("pageSelection.tooltip.individual.title", "Individual Pages"),
|
||||||
|
description: t("pageSelection.tooltip.individual.description", "Enter numbers separated by commas."),
|
||||||
|
bullets: [
|
||||||
|
t("pageSelection.tooltip.individual.bullet1", "<strong>1,3,5</strong> → selects pages 1, 3, 5"),
|
||||||
|
t("pageSelection.tooltip.individual.bullet2", "<strong>2,7,12</strong> → selects pages 2, 7, 12")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("pageSelection.tooltip.ranges.title", "Page Ranges"),
|
||||||
|
description: t("pageSelection.tooltip.ranges.description", "Use - for consecutive pages."),
|
||||||
|
bullets: [
|
||||||
|
t("pageSelection.tooltip.ranges.bullet1", "<strong>3-6</strong> → selects pages 3–6"),
|
||||||
|
t("pageSelection.tooltip.ranges.bullet2", "<strong>10-15</strong> → selects pages 10–15"),
|
||||||
|
t("pageSelection.tooltip.ranges.bullet3", "<strong>5-</strong> → selects pages 5 to end")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("pageSelection.tooltip.mathematical.title", "Mathematical Functions"),
|
||||||
|
description: t("pageSelection.tooltip.mathematical.description", "Use n in formulas for patterns."),
|
||||||
|
bullets: [
|
||||||
|
t("pageSelection.tooltip.mathematical.bullet2", "<strong>2n-1</strong> → all odd pages (1, 3, 5…)"),
|
||||||
|
t("pageSelection.tooltip.mathematical.bullet1", "<strong>2n</strong> → all even pages (2, 4, 6…)"),
|
||||||
|
t("pageSelection.tooltip.mathematical.bullet3", "<strong>3n</strong> → every 3rd page (3, 6, 9…)"),
|
||||||
|
t("pageSelection.tooltip.mathematical.bullet4", "<strong>4n-1</strong> → pages 3, 7, 11, 15…")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("pageSelection.tooltip.special.title", "Special Keywords"),
|
||||||
|
bullets: [
|
||||||
|
t("pageSelection.tooltip.special.bullet1", "<strong>all</strong> → selects all pages"),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("pageSelection.tooltip.complex.title", "Complex Combinations"),
|
||||||
|
description: t("pageSelection.tooltip.complex.description", "Mix different types."),
|
||||||
|
bullets: [
|
||||||
|
t("pageSelection.tooltip.complex.bullet1", "<strong>1,3-5,8,2n</strong> → pages 1, 3–5, 8, plus evens"),
|
||||||
|
t("pageSelection.tooltip.complex.bullet2", "<strong>10-,2n-1</strong> → from page 10 to end + odd pages")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import { FileWithUrl } from '../types/file';
|
import { FileWithUrl } from '../types/file';
|
||||||
import { StoredFile } from '../services/fileStorage';
|
import { StoredFile, fileStorage } from '../services/fileStorage';
|
||||||
|
import { downloadFiles } from '../utils/downloadUtils';
|
||||||
|
|
||||||
// Type for the context value - now contains everything directly
|
// Type for the context value - now contains everything directly
|
||||||
interface FileManagerContextValue {
|
interface FileManagerContextValue {
|
||||||
@ -11,16 +12,21 @@ interface FileManagerContextValue {
|
|||||||
selectedFiles: FileWithUrl[];
|
selectedFiles: FileWithUrl[];
|
||||||
filteredFiles: FileWithUrl[];
|
filteredFiles: FileWithUrl[];
|
||||||
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
fileInputRef: React.RefObject<HTMLInputElement | null>;
|
||||||
|
selectedFilesSet: Set<string>;
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
|
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
|
||||||
onLocalFileClick: () => void;
|
onLocalFileClick: () => void;
|
||||||
onFileSelect: (file: FileWithUrl) => void;
|
onFileSelect: (file: FileWithUrl, index: number, shiftKey?: boolean) => void;
|
||||||
onFileRemove: (index: number) => void;
|
onFileRemove: (index: number) => void;
|
||||||
onFileDoubleClick: (file: FileWithUrl) => void;
|
onFileDoubleClick: (file: FileWithUrl) => void;
|
||||||
onOpenFiles: () => void;
|
onOpenFiles: () => void;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
onFileInputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
onSelectAll: () => void;
|
||||||
|
onDeleteSelected: () => void;
|
||||||
|
onDownloadSelected: () => void;
|
||||||
|
onDownloadSingle: (file: FileWithUrl) => void;
|
||||||
|
|
||||||
// External props
|
// External props
|
||||||
recentFiles: FileWithUrl[];
|
recentFiles: FileWithUrl[];
|
||||||
@ -60,22 +66,29 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
|
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
|
||||||
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [lastClickedIndex, setLastClickedIndex] = useState<number | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Track blob URLs for cleanup
|
// Track blob URLs for cleanup
|
||||||
const createdBlobUrls = useRef<Set<string>>(new Set());
|
const createdBlobUrls = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
// Computed values (with null safety)
|
// Computed values (with null safety)
|
||||||
const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id || file.name));
|
const selectedFilesSet = new Set(selectedFileIds);
|
||||||
const filteredFiles = (recentFiles || []).filter(file =>
|
|
||||||
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
const selectedFiles = selectedFileIds.length === 0 ? [] :
|
||||||
);
|
(recentFiles || []).filter(file => selectedFilesSet.has(file.id || file.name));
|
||||||
|
|
||||||
|
const filteredFiles = !searchTerm ? recentFiles || [] :
|
||||||
|
(recentFiles || []).filter(file =>
|
||||||
|
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
const handleSourceChange = useCallback((source: 'recent' | 'local' | 'drive') => {
|
const handleSourceChange = useCallback((source: 'recent' | 'local' | 'drive') => {
|
||||||
setActiveSource(source);
|
setActiveSource(source);
|
||||||
if (source !== 'recent') {
|
if (source !== 'recent') {
|
||||||
setSelectedFileIds([]);
|
setSelectedFileIds([]);
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
|
setLastClickedIndex(null);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -83,19 +96,46 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFileSelect = useCallback((file: FileWithUrl) => {
|
const handleFileSelect = useCallback((file: FileWithUrl, currentIndex: number, shiftKey?: boolean) => {
|
||||||
setSelectedFileIds(prev => {
|
const fileId = file.id || file.name;
|
||||||
if (file.id) {
|
if (!fileId) return;
|
||||||
if (prev.includes(file.id)) {
|
|
||||||
return prev.filter(id => id !== file.id);
|
if (shiftKey && lastClickedIndex !== null) {
|
||||||
} else {
|
// Range selection with shift-click
|
||||||
return [...prev, file.id];
|
const startIndex = Math.min(lastClickedIndex, currentIndex);
|
||||||
|
const endIndex = Math.max(lastClickedIndex, currentIndex);
|
||||||
|
|
||||||
|
setSelectedFileIds(prev => {
|
||||||
|
const selectedSet = new Set(prev);
|
||||||
|
|
||||||
|
// Add all files in the range to selection
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
const rangeFileId = filteredFiles[i]?.id || filteredFiles[i]?.name;
|
||||||
|
if (rangeFileId) {
|
||||||
|
selectedSet.add(rangeFileId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return prev;
|
return Array.from(selectedSet);
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
}, []);
|
// Normal click behavior - optimized with Set for O(1) lookup
|
||||||
|
setSelectedFileIds(prev => {
|
||||||
|
const selectedSet = new Set(prev);
|
||||||
|
|
||||||
|
if (selectedSet.has(fileId)) {
|
||||||
|
selectedSet.delete(fileId);
|
||||||
|
} else {
|
||||||
|
selectedSet.add(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(selectedSet);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update last clicked index for future range selections
|
||||||
|
setLastClickedIndex(currentIndex);
|
||||||
|
}
|
||||||
|
}, [filteredFiles, lastClickedIndex]);
|
||||||
|
|
||||||
const handleFileRemove = useCallback((index: number) => {
|
const handleFileRemove = useCallback((index: number) => {
|
||||||
const fileToRemove = filteredFiles[index];
|
const fileToRemove = filteredFiles[index];
|
||||||
@ -152,6 +192,72 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}, [storeFile, onFilesSelected, refreshRecentFiles, onClose]);
|
}, [storeFile, onFilesSelected, refreshRecentFiles, onClose]);
|
||||||
|
|
||||||
|
const handleSelectAll = useCallback(() => {
|
||||||
|
const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length;
|
||||||
|
if (allFilesSelected) {
|
||||||
|
// Deselect all
|
||||||
|
setSelectedFileIds([]);
|
||||||
|
setLastClickedIndex(null);
|
||||||
|
} else {
|
||||||
|
// Select all filtered files
|
||||||
|
setSelectedFileIds(filteredFiles.map(file => file.id || file.name));
|
||||||
|
setLastClickedIndex(null);
|
||||||
|
}
|
||||||
|
}, [filteredFiles, selectedFileIds]);
|
||||||
|
|
||||||
|
const handleDeleteSelected = useCallback(async () => {
|
||||||
|
if (selectedFileIds.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get files to delete based on current filtered view
|
||||||
|
const filesToDelete = filteredFiles.filter(file =>
|
||||||
|
selectedFileIds.includes(file.id || file.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete files from storage
|
||||||
|
for (const file of filesToDelete) {
|
||||||
|
const lookupKey = file.id || file.name;
|
||||||
|
await fileStorage.deleteFile(lookupKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
setSelectedFileIds([]);
|
||||||
|
|
||||||
|
// Refresh the file list
|
||||||
|
await refreshRecentFiles();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete selected files:', error);
|
||||||
|
}
|
||||||
|
}, [selectedFileIds, filteredFiles, refreshRecentFiles]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleDownloadSelected = useCallback(async () => {
|
||||||
|
if (selectedFileIds.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get selected files
|
||||||
|
const selectedFilesToDownload = filteredFiles.filter(file =>
|
||||||
|
selectedFileIds.includes(file.id || file.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use generic download utility
|
||||||
|
await downloadFiles(selectedFilesToDownload, {
|
||||||
|
zipFilename: `selected-files-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download selected files:', error);
|
||||||
|
}
|
||||||
|
}, [selectedFileIds, filteredFiles]);
|
||||||
|
|
||||||
|
const handleDownloadSingle = useCallback(async (file: FileWithUrl) => {
|
||||||
|
try {
|
||||||
|
await downloadFiles([file]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download file:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
// Cleanup blob URLs when component unmounts
|
// Cleanup blob URLs when component unmounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -169,6 +275,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
setActiveSource('recent');
|
setActiveSource('recent');
|
||||||
setSelectedFileIds([]);
|
setSelectedFileIds([]);
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
|
setLastClickedIndex(null);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@ -180,6 +287,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
selectedFiles,
|
selectedFiles,
|
||||||
filteredFiles,
|
filteredFiles,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
|
selectedFilesSet,
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
onSourceChange: handleSourceChange,
|
onSourceChange: handleSourceChange,
|
||||||
@ -190,6 +298,10 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
onOpenFiles: handleOpenFiles,
|
onOpenFiles: handleOpenFiles,
|
||||||
onSearchChange: handleSearchChange,
|
onSearchChange: handleSearchChange,
|
||||||
onFileInputChange: handleFileInputChange,
|
onFileInputChange: handleFileInputChange,
|
||||||
|
onSelectAll: handleSelectAll,
|
||||||
|
onDeleteSelected: handleDeleteSelected,
|
||||||
|
onDownloadSelected: handleDownloadSelected,
|
||||||
|
onDownloadSingle: handleDownloadSingle,
|
||||||
|
|
||||||
// External props
|
// External props
|
||||||
recentFiles,
|
recentFiles,
|
||||||
|
@ -10,6 +10,12 @@ import ChangePermissions from '../tools/ChangePermissions';
|
|||||||
import RemovePassword from '../tools/RemovePassword';
|
import RemovePassword from '../tools/RemovePassword';
|
||||||
import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy';
|
import { SubcategoryId, ToolCategory, ToolRegistry } from './toolsTaxonomy';
|
||||||
import AddWatermark from '../tools/AddWatermark';
|
import AddWatermark from '../tools/AddWatermark';
|
||||||
|
import Repair from '../tools/Repair';
|
||||||
|
import SingleLargePage from '../tools/SingleLargePage';
|
||||||
|
import UnlockPdfForms from '../tools/UnlockPdfForms';
|
||||||
|
import RemoveCertificateSign from '../tools/RemoveCertificateSign';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Hook to get the translated tool registry
|
// Hook to get the translated tool registry
|
||||||
export function useFlatToolRegistry(): ToolRegistry {
|
export function useFlatToolRegistry(): ToolRegistry {
|
||||||
@ -94,11 +100,13 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
"unlock-pdf-forms": {
|
"unlock-pdf-forms": {
|
||||||
icon: <span className="material-symbols-rounded">preview_off</span>,
|
icon: <span className="material-symbols-rounded">preview_off</span>,
|
||||||
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
|
name: t("home.unlockPDFForms.title", "Unlock PDF Forms"),
|
||||||
component: null,
|
component: UnlockPdfForms,
|
||||||
view: "security",
|
view: "security",
|
||||||
description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."),
|
description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."),
|
||||||
category: ToolCategory.STANDARD_TOOLS,
|
category: ToolCategory.STANDARD_TOOLS,
|
||||||
subcategory: SubcategoryId.DOCUMENT_SECURITY
|
subcategory: SubcategoryId.DOCUMENT_SECURITY,
|
||||||
|
maxFiles: -1,
|
||||||
|
endpoints: ["unlock-pdf-forms"]
|
||||||
},
|
},
|
||||||
"manage-certificates": {
|
"manage-certificates": {
|
||||||
icon: <span className="material-symbols-rounded">license</span>,
|
icon: <span className="material-symbols-rounded">license</span>,
|
||||||
@ -230,11 +238,13 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
"single-large-page": {
|
"single-large-page": {
|
||||||
icon: <span className="material-symbols-rounded">looks_one</span>,
|
icon: <span className="material-symbols-rounded">looks_one</span>,
|
||||||
name: t("home.PdfToSinglePage.title", "PDF to Single Large Page"),
|
name: t("home.PdfToSinglePage.title", "PDF to Single Large Page"),
|
||||||
component: null,
|
component: SingleLargePage,
|
||||||
view: "format",
|
view: "format",
|
||||||
description: t("home.PdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
|
description: t("home.PdfToSinglePage.desc", "Merges all PDF pages into one large single page"),
|
||||||
category: ToolCategory.STANDARD_TOOLS,
|
category: ToolCategory.STANDARD_TOOLS,
|
||||||
subcategory: SubcategoryId.PAGE_FORMATTING
|
subcategory: SubcategoryId.PAGE_FORMATTING,
|
||||||
|
maxFiles: -1,
|
||||||
|
endpoints: ["pdf-to-single-page"]
|
||||||
},
|
},
|
||||||
"add-attachments": {
|
"add-attachments": {
|
||||||
icon: <span className="material-symbols-rounded">attachment</span>,
|
icon: <span className="material-symbols-rounded">attachment</span>,
|
||||||
@ -322,11 +332,13 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
"remove-certificate-sign": {
|
"remove-certificate-sign": {
|
||||||
icon: <span className="material-symbols-rounded">remove_moderator</span>,
|
icon: <span className="material-symbols-rounded">remove_moderator</span>,
|
||||||
name: t("home.removeCertSign.title", "Remove Certificate Signatures"),
|
name: t("home.removeCertSign.title", "Remove Certificate Signatures"),
|
||||||
component: null,
|
component: RemoveCertificateSign,
|
||||||
view: "security",
|
view: "security",
|
||||||
description: t("home.removeCertSign.desc", "Remove digital signatures from PDF documents"),
|
description: t("home.removeCertSign.desc", "Remove digital signatures from PDF documents"),
|
||||||
category: ToolCategory.STANDARD_TOOLS,
|
category: ToolCategory.STANDARD_TOOLS,
|
||||||
subcategory: SubcategoryId.REMOVAL
|
subcategory: SubcategoryId.REMOVAL,
|
||||||
|
maxFiles: -1,
|
||||||
|
endpoints: ["remove-certificate-sign"]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
@ -386,11 +398,13 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
"repair": {
|
"repair": {
|
||||||
icon: <span className="material-symbols-rounded">build</span>,
|
icon: <span className="material-symbols-rounded">build</span>,
|
||||||
name: t("home.repair.title", "Repair"),
|
name: t("home.repair.title", "Repair"),
|
||||||
component: null,
|
component: Repair,
|
||||||
view: "format",
|
view: "format",
|
||||||
description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
|
description: t("home.repair.desc", "Repair corrupted or damaged PDF files"),
|
||||||
category: ToolCategory.ADVANCED_TOOLS,
|
category: ToolCategory.ADVANCED_TOOLS,
|
||||||
subcategory: SubcategoryId.ADVANCED_FORMATTING
|
subcategory: SubcategoryId.ADVANCED_FORMATTING,
|
||||||
|
maxFiles: -1,
|
||||||
|
endpoints: ["repair"]
|
||||||
},
|
},
|
||||||
"detect-split-scanned-photos": {
|
"detect-split-scanned-photos": {
|
||||||
icon: <span className="material-symbols-rounded">scanner</span>,
|
icon: <span className="material-symbols-rounded">scanner</span>,
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { RemoveCertificateSignParameters } from './useRemoveCertificateSignParameters';
|
||||||
|
|
||||||
|
export const useRemoveCertificateSignOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const buildFormData = (parameters: RemoveCertificateSignParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useToolOperation<RemoveCertificateSignParameters>({
|
||||||
|
operationType: 'removeCertificateSign',
|
||||||
|
endpoint: '/api/v1/security/remove-cert-sign',
|
||||||
|
buildFormData,
|
||||||
|
filePrefix: t('removeCertSign.filenamePrefix', 'unsigned') + '_',
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('removeCertSign.error.failed', 'An error occurred while removing certificate signatures.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface RemoveCertificateSignParameters extends BaseParameters {
|
||||||
|
// Extends BaseParameters - ready for future parameter additions if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: RemoveCertificateSignParameters = {
|
||||||
|
// No parameters needed
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RemoveCertificateSignParametersHook = BaseParametersHook<RemoveCertificateSignParameters>;
|
||||||
|
|
||||||
|
export const useRemoveCertificateSignParameters = (): RemoveCertificateSignParametersHook => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'remove-certificate-sign',
|
||||||
|
});
|
||||||
|
};
|
23
frontend/src/hooks/tools/repair/useRepairOperation.ts
Normal file
23
frontend/src/hooks/tools/repair/useRepairOperation.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { RepairParameters } from './useRepairParameters';
|
||||||
|
|
||||||
|
export const useRepairOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const buildFormData = (parameters: RepairParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useToolOperation<RepairParameters>({
|
||||||
|
operationType: 'repair',
|
||||||
|
endpoint: '/api/v1/misc/repair',
|
||||||
|
buildFormData,
|
||||||
|
filePrefix: t('repair.filenamePrefix', 'repaired') + '_',
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('repair.error.failed', 'An error occurred while repairing the PDF.'))
|
||||||
|
});
|
||||||
|
};
|
20
frontend/src/hooks/tools/repair/useRepairParameters.ts
Normal file
20
frontend/src/hooks/tools/repair/useRepairParameters.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface RepairParameters extends BaseParameters {
|
||||||
|
// Extends BaseParameters - ready for future parameter additions if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: RepairParameters = {
|
||||||
|
// No parameters needed
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepairParametersHook = BaseParametersHook<RepairParameters>;
|
||||||
|
|
||||||
|
export const useRepairParameters = (): RepairParametersHook => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'repair',
|
||||||
|
// validateFn: optional custom validation if needed in future
|
||||||
|
});
|
||||||
|
};
|
46
frontend/src/hooks/tools/shared/useBaseParameters.ts
Normal file
46
frontend/src/hooks/tools/shared/useBaseParameters.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface BaseParametersHook<T> {
|
||||||
|
parameters: T;
|
||||||
|
updateParameter: <K extends keyof T>(parameter: K, value: T[K]) => void;
|
||||||
|
resetParameters: () => void;
|
||||||
|
validateParameters: () => boolean;
|
||||||
|
getEndpointName: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseParametersConfig<T> {
|
||||||
|
defaultParameters: T;
|
||||||
|
endpointName: string;
|
||||||
|
validateFn?: (params: T) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBaseParameters<T>(config: BaseParametersConfig<T>): BaseParametersHook<T> {
|
||||||
|
const [parameters, setParameters] = useState<T>(config.defaultParameters);
|
||||||
|
|
||||||
|
const updateParameter = useCallback(<K extends keyof T>(parameter: K, value: T[K]) => {
|
||||||
|
setParameters(prev => ({
|
||||||
|
...prev,
|
||||||
|
[parameter]: value,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetParameters = useCallback(() => {
|
||||||
|
setParameters(config.defaultParameters);
|
||||||
|
}, [config.defaultParameters]);
|
||||||
|
|
||||||
|
const validateParameters = useCallback(() => {
|
||||||
|
return config.validateFn ? config.validateFn(parameters) : true;
|
||||||
|
}, [parameters, config.validateFn]);
|
||||||
|
|
||||||
|
const getEndpointName = useCallback(() => {
|
||||||
|
return config.endpointName;
|
||||||
|
}, [config.endpointName]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
parameters,
|
||||||
|
updateParameter,
|
||||||
|
resetParameters,
|
||||||
|
validateParameters,
|
||||||
|
getEndpointName,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { SingleLargePageParameters } from './useSingleLargePageParameters';
|
||||||
|
|
||||||
|
export const useSingleLargePageOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const buildFormData = (parameters: SingleLargePageParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useToolOperation<SingleLargePageParameters>({
|
||||||
|
operationType: 'singleLargePage',
|
||||||
|
endpoint: '/api/v1/general/pdf-to-single-page',
|
||||||
|
buildFormData,
|
||||||
|
filePrefix: t('pdfToSinglePage.filenamePrefix', 'single_page') + '_',
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('pdfToSinglePage.error.failed', 'An error occurred while converting to single page.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface SingleLargePageParameters extends BaseParameters {
|
||||||
|
// Extends BaseParameters - ready for future parameter additions if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: SingleLargePageParameters = {
|
||||||
|
// No parameters needed
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SingleLargePageParametersHook = BaseParametersHook<SingleLargePageParameters>;
|
||||||
|
|
||||||
|
export const useSingleLargePageParameters = (): SingleLargePageParametersHook => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'pdf-to-single-page',
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { UnlockPdfFormsParameters } from './useUnlockPdfFormsParameters';
|
||||||
|
|
||||||
|
export const useUnlockPdfFormsOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const buildFormData = (parameters: UnlockPdfFormsParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
return useToolOperation<UnlockPdfFormsParameters>({
|
||||||
|
operationType: 'unlockPdfForms',
|
||||||
|
endpoint: '/api/v1/misc/unlock-pdf-forms',
|
||||||
|
buildFormData,
|
||||||
|
filePrefix: t('unlockPDFForms.filenamePrefix', 'unlocked_forms') + '_',
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('unlockPDFForms.error.failed', 'An error occurred while unlocking PDF forms.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface UnlockPdfFormsParameters extends BaseParameters {
|
||||||
|
// Extends BaseParameters - ready for future parameter additions if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: UnlockPdfFormsParameters = {
|
||||||
|
// No parameters needed
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnlockPdfFormsParametersHook = BaseParametersHook<UnlockPdfFormsParameters>;
|
||||||
|
|
||||||
|
export const useUnlockPdfFormsParameters = (): UnlockPdfFormsParametersHook => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'unlock-pdf-forms',
|
||||||
|
});
|
||||||
|
};
|
@ -191,6 +191,19 @@ export const mantineTheme = createTheme({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Tooltip: {
|
||||||
|
styles: {
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'var( --tooltip-title-bg)',
|
||||||
|
color: 'var( --tooltip-title-color)',
|
||||||
|
border: '1px solid var(--tooltip-borderp)',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: '500',
|
||||||
|
boxShadow: 'var(--shadow-md)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
Checkbox: {
|
Checkbox: {
|
||||||
styles: {
|
styles: {
|
||||||
|
80
frontend/src/tools/RemoveCertificateSign.tsx
Normal file
80
frontend/src/tools/RemoveCertificateSign.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||||
|
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
|
||||||
|
import { useRemoveCertificateSignParameters } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignParameters";
|
||||||
|
import { useRemoveCertificateSignOperation } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
|
||||||
|
const RemoveCertificateSign = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setCurrentMode } = useFileContext();
|
||||||
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
|
||||||
|
const removeCertificateSignParams = useRemoveCertificateSignParameters();
|
||||||
|
const removeCertificateSignOperation = useRemoveCertificateSignOperation();
|
||||||
|
|
||||||
|
// Endpoint validation
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(removeCertificateSignParams.getEndpointName());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
removeCertificateSignOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}, [removeCertificateSignParams.parameters]);
|
||||||
|
|
||||||
|
const handleRemoveSignature = async () => {
|
||||||
|
try {
|
||||||
|
await removeCertificateSignOperation.executeOperation(removeCertificateSignParams.parameters, selectedFiles);
|
||||||
|
if (removeCertificateSignOperation.files && onComplete) {
|
||||||
|
onComplete(removeCertificateSignOperation.files);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error.message : t("removeCertSign.error.failed", "Remove certificate signature operation failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
sessionStorage.setItem("previousMode", "removeCertificateSign");
|
||||||
|
setCurrentMode("viewer");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsReset = () => {
|
||||||
|
removeCertificateSignOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
setCurrentMode("removeCertificateSign");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
const hasResults = removeCertificateSignOperation.files.length > 0 || removeCertificateSignOperation.downloadUrl !== null;
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles,
|
||||||
|
isCollapsed: hasFiles || hasResults,
|
||||||
|
placeholder: t("removeCertSign.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
executeButton: {
|
||||||
|
text: t("removeCertSign.submit", "Remove Signature"),
|
||||||
|
isVisible: !hasResults,
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: handleRemoveSignature,
|
||||||
|
disabled: !removeCertificateSignParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: hasResults,
|
||||||
|
operation: removeCertificateSignOperation,
|
||||||
|
title: t("removeCertSign.results.title", "Certificate Removal Results"),
|
||||||
|
onFileClick: handleThumbnailClick,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemoveCertificateSign;
|
80
frontend/src/tools/Repair.tsx
Normal file
80
frontend/src/tools/Repair.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||||
|
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
|
||||||
|
import { useRepairParameters } from "../hooks/tools/repair/useRepairParameters";
|
||||||
|
import { useRepairOperation } from "../hooks/tools/repair/useRepairOperation";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
|
||||||
|
const Repair = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setCurrentMode } = useFileContext();
|
||||||
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
|
||||||
|
const repairParams = useRepairParameters();
|
||||||
|
const repairOperation = useRepairOperation();
|
||||||
|
|
||||||
|
// Endpoint validation
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(repairParams.getEndpointName());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
repairOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}, [repairParams.parameters]);
|
||||||
|
|
||||||
|
const handleRepair = async () => {
|
||||||
|
try {
|
||||||
|
await repairOperation.executeOperation(repairParams.parameters, selectedFiles);
|
||||||
|
if (repairOperation.files && onComplete) {
|
||||||
|
onComplete(repairOperation.files);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error.message : t("repair.error.failed", "Repair operation failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
sessionStorage.setItem("previousMode", "repair");
|
||||||
|
setCurrentMode("viewer");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsReset = () => {
|
||||||
|
repairOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
setCurrentMode("repair");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
const hasResults = repairOperation.files.length > 0 || repairOperation.downloadUrl !== null;
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles,
|
||||||
|
isCollapsed: hasFiles || hasResults,
|
||||||
|
placeholder: t("repair.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
executeButton: {
|
||||||
|
text: t("repair.submit", "Repair PDF"),
|
||||||
|
isVisible: !hasResults,
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: handleRepair,
|
||||||
|
disabled: !repairParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: hasResults,
|
||||||
|
operation: repairOperation,
|
||||||
|
title: t("repair.results.title", "Repair Results"),
|
||||||
|
onFileClick: handleThumbnailClick,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Repair;
|
80
frontend/src/tools/SingleLargePage.tsx
Normal file
80
frontend/src/tools/SingleLargePage.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||||
|
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
|
||||||
|
import { useSingleLargePageParameters } from "../hooks/tools/singleLargePage/useSingleLargePageParameters";
|
||||||
|
import { useSingleLargePageOperation } from "../hooks/tools/singleLargePage/useSingleLargePageOperation";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
|
||||||
|
const SingleLargePage = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setCurrentMode } = useFileContext();
|
||||||
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
|
||||||
|
const singleLargePageParams = useSingleLargePageParameters();
|
||||||
|
const singleLargePageOperation = useSingleLargePageOperation();
|
||||||
|
|
||||||
|
// Endpoint validation
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(singleLargePageParams.getEndpointName());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
singleLargePageOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}, [singleLargePageParams.parameters]);
|
||||||
|
|
||||||
|
const handleConvert = async () => {
|
||||||
|
try {
|
||||||
|
await singleLargePageOperation.executeOperation(singleLargePageParams.parameters, selectedFiles);
|
||||||
|
if (singleLargePageOperation.files && onComplete) {
|
||||||
|
onComplete(singleLargePageOperation.files);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error.message : t("pdfToSinglePage.error.failed", "Single large page operation failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
sessionStorage.setItem("previousMode", "single-large-page");
|
||||||
|
setCurrentMode("viewer");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsReset = () => {
|
||||||
|
singleLargePageOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
setCurrentMode("single-large-page");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
const hasResults = singleLargePageOperation.files.length > 0 || singleLargePageOperation.downloadUrl !== null;
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles,
|
||||||
|
isCollapsed: hasFiles || hasResults,
|
||||||
|
placeholder: t("pdfToSinglePage.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
executeButton: {
|
||||||
|
text: t("pdfToSinglePage.submit", "Convert To Single Page"),
|
||||||
|
isVisible: !hasResults,
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: handleConvert,
|
||||||
|
disabled: !singleLargePageParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: hasResults,
|
||||||
|
operation: singleLargePageOperation,
|
||||||
|
title: t("pdfToSinglePage.results.title", "Single Page Results"),
|
||||||
|
onFileClick: handleThumbnailClick,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SingleLargePage;
|
80
frontend/src/tools/UnlockPdfForms.tsx
Normal file
80
frontend/src/tools/UnlockPdfForms.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||||
|
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
|
||||||
|
import { useUnlockPdfFormsParameters } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsParameters";
|
||||||
|
import { useUnlockPdfFormsOperation } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation";
|
||||||
|
import { BaseToolProps } from "../types/tool";
|
||||||
|
|
||||||
|
const UnlockPdfForms = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setCurrentMode } = useFileContext();
|
||||||
|
const { selectedFiles } = useToolFileSelection();
|
||||||
|
|
||||||
|
const unlockPdfFormsParams = useUnlockPdfFormsParameters();
|
||||||
|
const unlockPdfFormsOperation = useUnlockPdfFormsOperation();
|
||||||
|
|
||||||
|
// Endpoint validation
|
||||||
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(unlockPdfFormsParams.getEndpointName());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
unlockPdfFormsOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
}, [unlockPdfFormsParams.parameters]);
|
||||||
|
|
||||||
|
const handleUnlock = async () => {
|
||||||
|
try {
|
||||||
|
await unlockPdfFormsOperation.executeOperation(unlockPdfFormsParams.parameters, selectedFiles);
|
||||||
|
if (unlockPdfFormsOperation.files && onComplete) {
|
||||||
|
onComplete(unlockPdfFormsOperation.files);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (onError) {
|
||||||
|
onError(error instanceof Error ? error.message : t("unlockPDFForms.error.failed", "Unlock PDF forms operation failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleThumbnailClick = (file: File) => {
|
||||||
|
onPreviewFile?.(file);
|
||||||
|
sessionStorage.setItem("previousMode", "unlockPdfForms");
|
||||||
|
setCurrentMode("viewer");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSettingsReset = () => {
|
||||||
|
unlockPdfFormsOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
setCurrentMode("unlockPdfForms");
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasFiles = selectedFiles.length > 0;
|
||||||
|
const hasResults = unlockPdfFormsOperation.files.length > 0 || unlockPdfFormsOperation.downloadUrl !== null;
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles,
|
||||||
|
isCollapsed: hasFiles || hasResults,
|
||||||
|
placeholder: t("unlockPDFForms.files.placeholder", "Select a PDF file in the main view to get started"),
|
||||||
|
},
|
||||||
|
steps: [],
|
||||||
|
executeButton: {
|
||||||
|
text: t("unlockPDFForms.submit", "Unlock Forms"),
|
||||||
|
isVisible: !hasResults,
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: handleUnlock,
|
||||||
|
disabled: !unlockPdfFormsParams.validateParameters() || !hasFiles || !endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: hasResults,
|
||||||
|
operation: unlockPdfFormsOperation,
|
||||||
|
title: t("unlockPDFForms.results.title", "Unlocked Forms Results"),
|
||||||
|
onFileClick: handleThumbnailClick,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnlockPdfForms;
|
@ -18,7 +18,11 @@ export type ModeType =
|
|||||||
| 'addPassword'
|
| 'addPassword'
|
||||||
| 'changePermissions'
|
| 'changePermissions'
|
||||||
| 'watermark'
|
| 'watermark'
|
||||||
| 'removePassword';
|
| 'removePassword'
|
||||||
|
| 'single-large-page'
|
||||||
|
| 'repair'
|
||||||
|
| 'unlockPdfForms'
|
||||||
|
| 'removeCertificateSign';
|
||||||
|
|
||||||
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
||||||
|
|
||||||
|
7
frontend/src/types/parameters.ts
Normal file
7
frontend/src/types/parameters.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Base parameter interfaces for reusable patterns
|
||||||
|
|
||||||
|
export interface BaseParameters {
|
||||||
|
// Base interface that all tool parameters should extend
|
||||||
|
// Provides a foundation for adding common properties across all tools
|
||||||
|
// Examples of future additions: userId, sessionId, commonFlags, etc.
|
||||||
|
}
|
152
frontend/src/utils/downloadUtils.ts
Normal file
152
frontend/src/utils/downloadUtils.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { FileWithUrl } from '../types/file';
|
||||||
|
import { fileStorage } from '../services/fileStorage';
|
||||||
|
import { zipFileService } from '../services/zipFileService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a blob as a file using browser download API
|
||||||
|
* @param blob - The blob to download
|
||||||
|
* @param filename - The filename for the download
|
||||||
|
*/
|
||||||
|
export function downloadBlob(blob: Blob, filename: string): void {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
// Clean up the blob URL
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a single file from IndexedDB storage
|
||||||
|
* @param file - The file object with storage information
|
||||||
|
* @throws Error if file cannot be retrieved from storage
|
||||||
|
*/
|
||||||
|
export async function downloadFileFromStorage(file: FileWithUrl): Promise<void> {
|
||||||
|
const lookupKey = file.id || file.name;
|
||||||
|
const storedFile = await fileStorage.getFile(lookupKey);
|
||||||
|
|
||||||
|
if (!storedFile) {
|
||||||
|
throw new Error(`File "${file.name}" not found in storage`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([storedFile.data], { type: storedFile.type });
|
||||||
|
downloadBlob(blob, storedFile.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads multiple files as individual downloads
|
||||||
|
* @param files - Array of files to download
|
||||||
|
*/
|
||||||
|
export async function downloadMultipleFiles(files: FileWithUrl[]): Promise<void> {
|
||||||
|
for (const file of files) {
|
||||||
|
await downloadFileFromStorage(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads multiple files as a single ZIP archive
|
||||||
|
* @param files - Array of files to include in ZIP
|
||||||
|
* @param zipFilename - Optional custom ZIP filename (defaults to timestamped name)
|
||||||
|
*/
|
||||||
|
export async function downloadFilesAsZip(files: FileWithUrl[], zipFilename?: string): Promise<void> {
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error('No files provided for ZIP download');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert stored files to File objects
|
||||||
|
const fileObjects: File[] = [];
|
||||||
|
for (const fileWithUrl of files) {
|
||||||
|
const lookupKey = fileWithUrl.id || fileWithUrl.name;
|
||||||
|
const storedFile = await fileStorage.getFile(lookupKey);
|
||||||
|
|
||||||
|
if (storedFile) {
|
||||||
|
const file = new File([storedFile.data], storedFile.name, {
|
||||||
|
type: storedFile.type,
|
||||||
|
lastModified: storedFile.lastModified
|
||||||
|
});
|
||||||
|
fileObjects.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileObjects.length === 0) {
|
||||||
|
throw new Error('No valid files found in storage for ZIP download');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate default filename if not provided
|
||||||
|
const finalZipFilename = zipFilename ||
|
||||||
|
`files-${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.zip`;
|
||||||
|
|
||||||
|
// Create and download ZIP
|
||||||
|
const { zipFile } = await zipFileService.createZipFromFiles(fileObjects, finalZipFilename);
|
||||||
|
downloadBlob(zipFile, finalZipFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smart download function that handles single or multiple files appropriately
|
||||||
|
* - Single file: Downloads directly
|
||||||
|
* - Multiple files: Downloads as ZIP
|
||||||
|
* @param files - Array of files to download
|
||||||
|
* @param options - Download options
|
||||||
|
*/
|
||||||
|
export async function downloadFiles(
|
||||||
|
files: FileWithUrl[],
|
||||||
|
options: {
|
||||||
|
forceZip?: boolean;
|
||||||
|
zipFilename?: string;
|
||||||
|
multipleAsIndividual?: boolean;
|
||||||
|
} = {}
|
||||||
|
): Promise<void> {
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error('No files provided for download');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 1 && !options.forceZip) {
|
||||||
|
// Single file download
|
||||||
|
await downloadFileFromStorage(files[0]);
|
||||||
|
} else if (options.multipleAsIndividual) {
|
||||||
|
// Multiple individual downloads
|
||||||
|
await downloadMultipleFiles(files);
|
||||||
|
} else {
|
||||||
|
// ZIP download (default for multiple files)
|
||||||
|
await downloadFilesAsZip(files, options.zipFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a File object directly (for files already in memory)
|
||||||
|
* @param file - The File object to download
|
||||||
|
* @param filename - Optional custom filename
|
||||||
|
*/
|
||||||
|
export function downloadFileObject(file: File, filename?: string): void {
|
||||||
|
downloadBlob(file, filename || file.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads text content as a file
|
||||||
|
* @param content - Text content to download
|
||||||
|
* @param filename - Filename for the download
|
||||||
|
* @param mimeType - MIME type (defaults to text/plain)
|
||||||
|
*/
|
||||||
|
export function downloadTextAsFile(
|
||||||
|
content: string,
|
||||||
|
filename: string,
|
||||||
|
mimeType: string = 'text/plain'
|
||||||
|
): void {
|
||||||
|
const blob = new Blob([content], { type: mimeType });
|
||||||
|
downloadBlob(blob, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads JSON data as a file
|
||||||
|
* @param data - Data to serialize and download
|
||||||
|
* @param filename - Filename for the download
|
||||||
|
*/
|
||||||
|
export function downloadJsonAsFile(data: any, filename: string): void {
|
||||||
|
const content = JSON.stringify(data, null, 2);
|
||||||
|
downloadTextAsFile(content, filename, 'application/json');
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user