mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
parking this to work on OCR, I added all the icons, tool names / descriptions and changed the styling of the all tools bar
This commit is contained in:
parent
eaaaf5c06c
commit
6f7a157340
@ -363,6 +363,10 @@
|
||||
"title": "Add image",
|
||||
"desc": "Adds a image onto a set location on the PDF"
|
||||
},
|
||||
"attachments": {
|
||||
"title": "Add Attachments",
|
||||
"desc": "Add or remove embedded files (attachments) to/from a PDF"
|
||||
},
|
||||
"watermark": {
|
||||
"title": "Add Watermark",
|
||||
"desc": "Add a custom watermark to your PDF document."
|
||||
@ -578,6 +582,34 @@
|
||||
"replaceColorPdf": {
|
||||
"title": "Advanced Colour options",
|
||||
"desc": "Replace colour for text and background in PDF and invert full colour of pdf to reduce file size"
|
||||
},
|
||||
"EMLToPDF": {
|
||||
"title": "Email to PDF",
|
||||
"desc": "Converts email (EML) files to PDF format including headers, body, and inline images"
|
||||
},
|
||||
"fakeScan": {
|
||||
"title": "Fake Scan",
|
||||
"desc": "Create a PDF that looks like it was scanned"
|
||||
},
|
||||
"editTableOfContents": {
|
||||
"title": "Edit Table of Contents",
|
||||
"desc": "Add or edit bookmarks and table of contents in PDF documents"
|
||||
},
|
||||
"automate": {
|
||||
"title": "Automate",
|
||||
"desc": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
|
||||
},
|
||||
"manageCertificates": {
|
||||
"title": "Manage Certificates",
|
||||
"desc": "Import, export, or delete digital certificate files used for signing PDFs."
|
||||
},
|
||||
"read": {
|
||||
"title": "Read",
|
||||
"desc": "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
|
||||
},
|
||||
"reorganizePages": {
|
||||
"title": "Reorganize Pages",
|
||||
"desc": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
|
||||
}
|
||||
},
|
||||
"viewPdf": {
|
||||
@ -691,6 +723,15 @@
|
||||
"upload": "Add image",
|
||||
"submit": "Add image"
|
||||
},
|
||||
"attachments": {
|
||||
"tags": "attachments,add,remove,embed,file",
|
||||
"title": "Add Attachments",
|
||||
"header": "Add Attachments",
|
||||
"add": "Add Attachment",
|
||||
"remove": "Remove Attachment",
|
||||
"embed": "Embed Attachment",
|
||||
"submit": "Add Attachments"
|
||||
},
|
||||
"watermark": {
|
||||
"tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo",
|
||||
"title": "Add Watermark",
|
||||
@ -1526,7 +1567,7 @@
|
||||
"title": "How we use Cookies",
|
||||
"description": {
|
||||
"1": "We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you'll love.",
|
||||
"2": "If you’d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly."
|
||||
"2": "If you'd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly."
|
||||
},
|
||||
"acceptAllBtn": "Okay",
|
||||
"acceptNecessaryBtn": "No Thanks",
|
||||
@ -1550,7 +1591,7 @@
|
||||
"1": "Strictly Necessary Cookies",
|
||||
"2": "Always Enabled"
|
||||
},
|
||||
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off."
|
||||
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can't be turned off."
|
||||
},
|
||||
"analytics": {
|
||||
"title": "Analytics",
|
||||
|
@ -144,7 +144,10 @@ const QuickAccessBar = ({
|
||||
{
|
||||
id: 'automate',
|
||||
name: 'Automate',
|
||||
icon: <AutoAwesomeIcon sx={{ fontSize: "1.5rem" }} />,
|
||||
icon:
|
||||
<span className="material-symbols-rounded font-size-20">
|
||||
automation
|
||||
</span>,
|
||||
tooltip: 'Automate workflows',
|
||||
size: 'lg',
|
||||
isRound: false,
|
||||
@ -214,6 +217,9 @@ const QuickAccessBar = ({
|
||||
return (
|
||||
<div
|
||||
className={`h-screen flex flex-col w-20 quick-access-bar-main ${isRainbowMode ? 'rainbow-mode' : ''}`}
|
||||
style={{
|
||||
borderRight: '1px solid var(--border-default)'
|
||||
}}
|
||||
>
|
||||
{/* Fixed header outside scrollable area */}
|
||||
<div className="quick-access-header">
|
||||
|
27
frontend/src/components/tools/ToolPicker.css
Normal file
27
frontend/src/components/tools/ToolPicker.css
Normal file
@ -0,0 +1,27 @@
|
||||
.tool-picker-scrollable {
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--mantine-color-gray-4) transparent;
|
||||
}
|
||||
|
||||
.tool-picker-scrollable::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.tool-picker-scrollable::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tool-picker-scrollable::-webkit-scrollbar-thumb {
|
||||
background-color: var(--mantine-color-gray-4);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.tool-picker-scrollable::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--mantine-color-gray-5);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin: 1rem;
|
||||
}
|
@ -1,7 +1,21 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Text, Stack, Button, TextInput, Group } from "@mantine/core";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { Box, Text, Stack, Button, TextInput, Group, Tooltip, Collapse, ActionIcon } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ToolRegistry } from "../../types/tool";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import { baseToolRegistry } from "../../data/toolRegistry";
|
||||
import "./ToolPicker.css";
|
||||
|
||||
type Tool = {
|
||||
icon: React.ReactNode;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
type ToolRegistry = {
|
||||
[id: string]: Tool;
|
||||
};
|
||||
|
||||
interface ToolPickerProps {
|
||||
selectedToolKey: string | null;
|
||||
@ -9,46 +23,219 @@ interface ToolPickerProps {
|
||||
toolRegistry: ToolRegistry;
|
||||
}
|
||||
|
||||
interface GroupedTools {
|
||||
[category: string]: {
|
||||
[subcategory: string]: Array<{ id: string; tool: Tool }>;
|
||||
};
|
||||
}
|
||||
|
||||
const ToolPicker = ({ selectedToolKey, onSelect, toolRegistry }: ToolPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
|
||||
const filteredTools = Object.entries(toolRegistry).filter(([_, { name }]) =>
|
||||
name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
// Group tools by category and subcategory in a single pass - O(n)
|
||||
const groupedTools = useMemo(() => {
|
||||
const grouped: GroupedTools = {};
|
||||
|
||||
return (
|
||||
<Box >
|
||||
<TextInput
|
||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
mb="md"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Stack align="flex-start">
|
||||
{filteredTools.length === 0 ? (
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("toolPicker.noToolsFound", "No tools found")}
|
||||
</Text>
|
||||
) : (
|
||||
filteredTools.map(([id, { icon, name }]) => (
|
||||
<Button
|
||||
Object.entries(toolRegistry).forEach(([id, tool]) => {
|
||||
// Get category and subcategory from the base registry
|
||||
const baseTool = baseToolRegistry[id as keyof typeof baseToolRegistry];
|
||||
const category = baseTool?.category || "Other";
|
||||
const subcategory = baseTool?.subcategory || "General";
|
||||
|
||||
if (!grouped[category]) {
|
||||
grouped[category] = {};
|
||||
}
|
||||
if (!grouped[category][subcategory]) {
|
||||
grouped[category][subcategory] = [];
|
||||
}
|
||||
|
||||
grouped[category][subcategory].push({ id, tool });
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}, [toolRegistry]);
|
||||
|
||||
// Sort categories in custom order and subcategories alphabetically - O(c * s * log(s))
|
||||
const sortedCategories = useMemo(() => {
|
||||
const categoryOrder = ['RECOMMENDED TOOLS', 'STANDARD TOOLS', 'ADVANCED TOOLS'];
|
||||
|
||||
return Object.entries(groupedTools)
|
||||
.map(([category, subcategories]) => ({
|
||||
category,
|
||||
subcategories: Object.entries(subcategories)
|
||||
.sort(([a], [b]) => a.localeCompare(b)) // Sort subcategories alphabetically
|
||||
.map(([subcategory, tools]) => ({
|
||||
subcategory,
|
||||
tools: tools.sort((a, b) => a.tool.name.localeCompare(b.tool.name)) // Sort tools alphabetically
|
||||
}))
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const aIndex = categoryOrder.indexOf(a.category.toUpperCase());
|
||||
const bIndex = categoryOrder.indexOf(b.category.toUpperCase());
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
}, [groupedTools, t]);
|
||||
|
||||
// Filter tools based on search - O(n)
|
||||
const filteredCategories = useMemo(() => {
|
||||
if (!search.trim()) return sortedCategories;
|
||||
|
||||
return sortedCategories.map(({ category, subcategories }) => ({
|
||||
category,
|
||||
subcategories: subcategories.map(({ subcategory, tools }) => ({
|
||||
subcategory,
|
||||
tools: tools.filter(({ tool }) =>
|
||||
tool.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
tool.description.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
})).filter(({ tools }) => tools.length > 0)
|
||||
})).filter(({ subcategories }) => subcategories.length > 0);
|
||||
}, [sortedCategories, search, t]);
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
setExpandedCategories(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(category)) {
|
||||
newSet.delete(category);
|
||||
} else {
|
||||
newSet.add(category);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
const renderToolButton = (id: string, tool: Tool, index: number) => (
|
||||
<Tooltip
|
||||
key={id}
|
||||
label={tool.description}
|
||||
position="right"
|
||||
withArrow
|
||||
openDelay={500}
|
||||
>
|
||||
<Button
|
||||
variant={selectedToolKey === id ? "filled" : "subtle"}
|
||||
onClick={() => onSelect(id)}
|
||||
size="md"
|
||||
radius="md"
|
||||
leftSection={icon}
|
||||
leftSection={tool.icon}
|
||||
fullWidth
|
||||
justify="flex-start"
|
||||
style={{ borderRadius: '0' }}
|
||||
>
|
||||
{name}
|
||||
<span style={{ marginRight: '8px', opacity: 0.6, fontSize: '0.8em' }}>
|
||||
{index + 1}.
|
||||
</span>
|
||||
{tool.name}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
padding: '0'
|
||||
}}>
|
||||
<TextInput
|
||||
placeholder={t("toolPicker.searchPlaceholder", "Search tools...")}
|
||||
value={search}
|
||||
radius="md"
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
autoComplete="off"
|
||||
className="search-input rounded-lg"
|
||||
leftSection={<SearchIcon sx={{ fontSize: 16, color: 'var(--search-text)' }} />}
|
||||
/>
|
||||
<Box
|
||||
className="tool-picker-scrollable"
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
minHeight: 0,
|
||||
maxHeight: 'calc(100vh - 200px)'
|
||||
}}
|
||||
>
|
||||
<Stack align="flex-start" gap="xs">
|
||||
{filteredCategories.length === 0 ? (
|
||||
<Text c="dimmed" size="sm">
|
||||
{t("toolPicker.noToolsFound", "No tools found")}
|
||||
</Text>
|
||||
) : (
|
||||
filteredCategories.map(({ category, subcategories }) => (
|
||||
<Box key={category} style={{ width: '100%' }}>
|
||||
{/* Category Header */}
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => toggleCategory(category)}
|
||||
rightSection={
|
||||
<div style={{
|
||||
transition: 'transform 0.2s ease',
|
||||
transform: expandedCategories.has(category) ? 'rotate(90deg)' : 'rotate(0deg)'
|
||||
}}>
|
||||
<ChevronRightIcon sx={{ fontSize: 16, color: 'var(--text-primary)' }} />
|
||||
</div>
|
||||
}
|
||||
fullWidth
|
||||
justify="space-between"
|
||||
style={{
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: 'var(--bg-toolbar)',
|
||||
marginBottom: '0',
|
||||
borderTop: '1px solid var(--border-default)',
|
||||
borderBottom: '1px solid var(--border-default)',
|
||||
borderRadius: '0',
|
||||
padding: '0.75rem 1rem',
|
||||
color: 'var(--text-primary)'
|
||||
}}
|
||||
>
|
||||
{category.toUpperCase()}
|
||||
</Button>
|
||||
|
||||
{/* Subcategories */}
|
||||
<Collapse in={expandedCategories.has(category)}>
|
||||
<Stack gap="xs" style={{ paddingLeft: '1rem', paddingRight: '1rem' }}>
|
||||
{subcategories.map(({ subcategory, tools }) => (
|
||||
<Box key={subcategory}>
|
||||
{/* Subcategory Header (only show if there are multiple subcategories) */}
|
||||
{subcategories.length > 1 && (
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}
|
||||
style={{
|
||||
marginBottom: '4px',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '0.75rem',
|
||||
borderBottom: '1px solid var(--border-default)',
|
||||
paddingBottom: '0.5rem',
|
||||
marginLeft: '1rem',
|
||||
marginRight: '1rem',
|
||||
color: 'var(--text-secondary)'
|
||||
}}
|
||||
>
|
||||
{subcategory}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Tools in this subcategory */}
|
||||
<Stack gap="xs">
|
||||
{tools.map(({ id, tool }, index) =>
|
||||
renderToolButton(id, tool, index)
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
84
frontend/src/data/toolRegistry.tsx
Normal file
84
frontend/src/data/toolRegistry.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import SplitPdfPanel from "../tools/Split";
|
||||
import CompressPdfPanel from "../tools/Compress";
|
||||
import MergePdfPanel from "../tools/Merge";
|
||||
|
||||
export type ToolRegistryEntry = {
|
||||
icon: React.ReactNode;
|
||||
name: string;
|
||||
component: React.ComponentType<any> | null;
|
||||
view: string;
|
||||
description: string;
|
||||
category: string;
|
||||
subcategory: string | null;
|
||||
};
|
||||
|
||||
export type ToolRegistry = {
|
||||
[key: string]: ToolRegistryEntry;
|
||||
};
|
||||
|
||||
export const baseToolRegistry: ToolRegistry = {
|
||||
"add-attachments": { icon: <span className="material-symbols-rounded">attachment</span>, name: "home.attachments.title", component: null, view: "format", description: "home.attachments.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"add-image": { icon: <span className="material-symbols-rounded">image</span>, name: "home.addImage.title", component: null, view: "format", description: "home.addImage.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"add-page-numbers": { icon: <span className="material-symbols-rounded">123</span>, name: "home.add-page-numbers.title", component: null, view: "format", description: "home.add-page-numbers.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"add-password": { icon: <span className="material-symbols-rounded">password</span>, name: "home.addPassword.title", component: null, view: "security", description: "home.addPassword.desc", category: "Standard Tools", subcategory: "Document Security" },
|
||||
"add-stamp": { icon: <span className="material-symbols-rounded">approval</span>, name: "home.AddStampRequest.title", component: null, view: "format", description: "home.AddStampRequest.desc", category: "Standard Tools", subcategory: "Document Security" },
|
||||
"add-watermark": { icon: <span className="material-symbols-rounded">branding_watermark</span>, name: "home.watermark.title", component: null, view: "format", description: "home.watermark.desc", category: "Standard Tools", subcategory: "Document Security" },
|
||||
"adjust-colors-contrast": { icon: <span className="material-symbols-rounded">palette</span>, name: "home.adjust-contrast.title", component: null, view: "format", description: "home.adjust-contrast.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"adjust-page-size-scale": { icon: <span className="material-symbols-rounded">crop_free</span>, name: "home.scalePages.title", component: null, view: "format", description: "home.scalePages.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"auto-rename-pdf-file": { icon: <span className="material-symbols-rounded">match_word</span>, name: "home.auto-rename.title", component: null, view: "format", description: "home.auto-rename.desc", category: "Advanced Tools", subcategory: "Automation" },
|
||||
"auto-split-by-size-count": { icon: <span className="material-symbols-rounded">content_cut</span>, name: "home.autoSizeSplitPDF.title", component: null, view: "format", description: "home.autoSizeSplitPDF.desc", category: "Advanced Tools", subcategory: "Automation" },
|
||||
"auto-split-pages": { icon: <span className="material-symbols-rounded">split_scene_right</span>, name: "home.autoSplitPDF.title", component: null, view: "format", description: "home.autoSplitPDF.desc", category: "Advanced Tools", subcategory: "Automation" },
|
||||
"automate": { icon: <span className="material-symbols-rounded">automation</span>, name: "home.automate.title", component: null, view: "format", description: "home.automate.desc", category: "Advanced Tools", subcategory: "Automation" },
|
||||
"certSign": { icon: <span className="material-symbols-rounded">workspace_premium</span>, name: "home.certSign.title", component: null, view: "sign", description: "home.certSign.desc", category: "Standard Tools", subcategory: "Signing" },
|
||||
"change-metadata": { icon: <span className="material-symbols-rounded">assignment</span>, name: "home.changeMetadata.title", component: null, view: "format", description: "home.changeMetadata.desc", category: "Standard Tools", subcategory: "Document Review" },
|
||||
"change-permissions": { icon: <span className="material-symbols-rounded">admin_panel_settings</span>, name: "home.permissions.title", component: null, view: "security", description: "home.permissions.desc", category: "Standard Tools", subcategory: "Document Review" },
|
||||
"compare": { icon: <span className="material-symbols-rounded">compare</span>, name: "home.compare.title", component: null, view: "format", description: "home.compare.desc", category: "Recommended Tools", subcategory: null },
|
||||
"compressPdfs": { icon: <span className="material-symbols-rounded">zoom_in_map</span>, name: "home.compressPdfs.title", component: CompressPdfPanel, view: "compress", description: "home.compressPdfs.desc", category: "Recommended Tools", subcategory: null },
|
||||
"convert": { icon: <span className="material-symbols-rounded">sync_alt</span>, name: "home.fileToPDF.title", component: null, view: "convert", description: "home.fileToPDF.desc", category: "Recommended Tools", subcategory: null },
|
||||
"cropPdf": { icon: <span className="material-symbols-rounded">crop</span>, name: "home.crop.title", component: null, view: "format", description: "home.crop.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"detect-split-scanned-photos": { icon: <span className="material-symbols-rounded">scanner</span>, name: "home.ScannerImageSplit.title", component: null, view: "format", description: "home.ScannerImageSplit.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"edit-table-of-contents": { icon: <span className="material-symbols-rounded">bookmark_add</span>, name: "home.editTableOfContents.title", component: null, view: "format", description: "home.editTableOfContents.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"extract-images": { icon: <span className="material-symbols-rounded">filter</span>, name: "home.extractImages.title", component: null, view: "extract", description: "home.extractImages.desc", category: "Standard Tools", subcategory: "Extraction" },
|
||||
"extract-pages": { icon: <span className="material-symbols-rounded">upload</span>, name: "home.extractPage.title", component: null, view: "extract", description: "home.extractPage.desc", category: "Standard Tools", subcategory: "Extraction" },
|
||||
"flatten": { icon: <span className="material-symbols-rounded">layers_clear</span>, name: "home.flatten.title", component: null, view: "format", description: "home.flatten.desc", category: "Standard Tools", subcategory: "Document Security" },
|
||||
"get-all-info-on-pdf": { icon: <span className="material-symbols-rounded">fact_check</span>, name: "home.getPdfInfo.title", component: null, view: "extract", description: "home.getPdfInfo.desc", category: "Standard Tools", subcategory: "Verification" },
|
||||
"manage-certificates": { icon: <span className="material-symbols-rounded">license</span>, name: "home.manageCertificates.title", component: null, view: "security", description: "home.manageCertificates.desc", category: "Standard Tools", subcategory: "Document Security" },
|
||||
"mergePdfs": { icon: <span className="material-symbols-rounded">library_add</span>, name: "home.merge.title", component: MergePdfPanel, view: "merge", description: "home.merge.desc", category: "Recommended Tools", subcategory: null },
|
||||
"multi-page-layout": { icon: <span className="material-symbols-rounded">dashboard</span>, name: "home.pageLayout.title", component: null, view: "format", description: "home.pageLayout.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"multi-tool": { icon: <span className="material-symbols-rounded">dashboard_customize</span>, name: "home.multiTool.title", component: null, view: "pageEditor", description: "home.multiTool.desc", category: "Recommended Tools", subcategory: null },
|
||||
"ocr": { icon: <span className="material-symbols-rounded">quick_reference_all</span>, name: "home.ocr.title", component: null, view: "convert", description: "home.ocr.desc", category: "Recommended Tools", subcategory: null },
|
||||
"overlay-pdfs": { icon: <span className="material-symbols-rounded">layers</span>, name: "home.overlay-pdfs.title", component: null, view: "format", description: "home.overlay-pdfs.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"read": { icon: <span className="material-symbols-rounded">article</span>, name: "home.read.title", component: null, view: "view", description: "home.read.desc", category: "Standard Tools", subcategory: "Document Review" },
|
||||
"redact": { icon: <span className="material-symbols-rounded">visibility_off</span>, name: "home.redact.title", component: null, view: "redact", description: "home.redact.desc", category: "Recommended Tools", subcategory: null },
|
||||
"remove": { icon: <span className="material-symbols-rounded">delete</span>, name: "home.removePages.title", component: null, view: "remove", description: "home.removePages.desc", category: "Standard Tools", subcategory: "Removal" },
|
||||
"remove-annotations": { icon: <span className="material-symbols-rounded">thread_unread</span>, name: "home.removeAnnotations.title", component: null, view: "remove", description: "home.removeAnnotations.desc", category: "Standard Tools", subcategory: "Removal" },
|
||||
"remove-blank-pages": { icon: <span className="material-symbols-rounded">scan_delete</span>, name: "home.removeBlanks.title", component: null, view: "remove", description: "home.removeBlanks.desc", category: "Standard Tools", subcategory: "Removal" },
|
||||
"remove-certificate-sign": { icon: <span className="material-symbols-rounded">remove_moderator</span>, name: "home.removeCertSign.title", component: null, view: "security", description: "home.removeCertSign.desc", category: "Standard Tools", subcategory: "Removal" },
|
||||
"remove-image": { icon: <span className="material-symbols-rounded">remove_selection</span>, name: "home.removeImagePdf.title", component: null, view: "format", description: "home.removeImagePdf.desc", category: "Standard Tools", subcategory: "Removal" },
|
||||
"remove-password": { icon: <span className="material-symbols-rounded">lock_open_right</span>, name: "home.removePassword.title", component: null, view: "security", description: "home.removePassword.desc", category: "Standard Tools", subcategory: "Removal" },
|
||||
"repair": { icon: <span className="material-symbols-rounded">build</span>, name: "home.repair.title", component: null, view: "format", description: "home.repair.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"replace-and-invert-color": { icon: <span className="material-symbols-rounded">format_color_fill</span>, name: "home.replaceColorPdf.title", component: null, view: "format", description: "home.replaceColorPdf.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"reorganize-pages": { icon: <span className="material-symbols-rounded">move_down</span>, name: "home.reorganizePages.title", component: null, view: "pageEditor", description: "home.reorganizePages.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"rotate": { icon: <span className="material-symbols-rounded">rotate_right</span>, name: "home.rotate.title", component: null, view: "format", description: "home.rotate.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"sanitize": { icon: <span className="material-symbols-rounded">sanitizer</span>, name: "home.sanitizePdf.title", component: null, view: "security", description: "home.sanitizePdf.desc", category: "Standard Tools", subcategory: "Document Security" },
|
||||
"scanner-effect": { icon: <span className="material-symbols-rounded">scanner</span>, name: "home.fakeScan.title", component: null, view: "format", description: "home.fakeScan.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"show-javascript": { icon: <span className="material-symbols-rounded">javascript</span>, name: "home.showJS.title", component: null, view: "extract", description: "home.showJS.desc", category: "Advanced Tools", subcategory: "Developer Tools" },
|
||||
"sign": { icon: <span className="material-symbols-rounded">signature</span>, name: "home.sign.title", component: null, view: "sign", description: "home.sign.desc", category: "Standard Tools", subcategory: "Signing" },
|
||||
"single-large-page": { icon: <span className="material-symbols-rounded">looks_one</span>, name: "home.PdfToSinglePage.title", component: null, view: "format", description: "home.PdfToSinglePage.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"split": { icon: <span className="material-symbols-rounded">content_cut</span>, name: "home.split.title", component: null, view: "format", description: "home.split.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"split-by-chapters": { icon: <span className="material-symbols-rounded">collections_bookmark</span>, name: "home.splitPdfByChapters.title", component: null, view: "format", description: "home.splitPdfByChapters.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"split-by-sections": { icon: <span className="material-symbols-rounded">grid_on</span>, name: "home.split-by-sections.title", component: null, view: "format", description: "home.split-by-sections.desc", category: "Advanced Tools", subcategory: "Advanced Formatting" },
|
||||
"splitPdf": { icon: <span className="material-symbols-rounded">content_cut</span>, name: "home.split.title", component: SplitPdfPanel, view: "split", description: "home.split.desc", category: "Standard Tools", subcategory: "Page Formatting" },
|
||||
"unlock-pdf-forms": { icon: <span className="material-symbols-rounded">preview_off</span>, name: "home.unlockPDFForms.title", component: null, view: "security", description: "home.unlockPDFForms.desc", category: "Standard Tools", subcategory: "Document Security" },
|
||||
"validate-pdf-signature": { icon: <span className="material-symbols-rounded">verified</span>, name: "home.validateSignature.title", component: null, view: "security", description: "home.validateSignature.desc", category: "Standard Tools", subcategory: "Verification" },
|
||||
"view-pdf": { icon: <span className="material-symbols-rounded">article</span>, name: "home.viewPdf.title", component: null, view: "view", description: "home.viewPdf.desc", category: "Recommended Tools", subcategory: null
|
||||
}
|
||||
};
|
||||
|
||||
export const toolEndpoints: Record<string, string[]> = {
|
||||
split: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"],
|
||||
compressPdfs: ["compress-pdf"],
|
||||
merge: ["merge-pdfs"],
|
||||
// Add more endpoint mappings as needed
|
||||
};
|
@ -1,38 +1,11 @@
|
||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
|
||||
|
||||
|
||||
// Add entry here with maxFiles, endpoints, and lazy component
|
||||
const toolDefinitions: Record<string, ToolDefinition> = {
|
||||
split: {
|
||||
id: "split",
|
||||
icon: <ContentCutIcon />,
|
||||
component: React.lazy(() => import("../tools/Split")),
|
||||
maxFiles: 1,
|
||||
category: "manipulation",
|
||||
description: "Split PDF files into smaller parts",
|
||||
endpoints: ["split-pages", "split-pdf-by-sections", "split-by-size-or-count", "split-pdf-by-chapters"]
|
||||
},
|
||||
compress: {
|
||||
id: "compress",
|
||||
icon: <ZoomInMapIcon />,
|
||||
component: React.lazy(() => import("../tools/Compress")),
|
||||
maxFiles: -1,
|
||||
category: "optimization",
|
||||
description: "Reduce PDF file size",
|
||||
endpoints: ["compress-pdf"]
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
import { baseToolRegistry, toolEndpoints, type ToolRegistry, type ToolRegistryEntry } from "../data/toolRegistry";
|
||||
|
||||
interface ToolManagementResult {
|
||||
selectedToolKey: string | null;
|
||||
selectedTool: Tool | null;
|
||||
selectedTool: ToolRegistryEntry | null;
|
||||
toolSelectedFileIds: string[];
|
||||
toolRegistry: ToolRegistry;
|
||||
selectTool: (toolKey: string) => void;
|
||||
@ -47,30 +20,30 @@ export const useToolManagement = (): ToolManagementResult => {
|
||||
const [toolSelectedFileIds, setToolSelectedFileIds] = useState<string[]>([]);
|
||||
|
||||
const allEndpoints = Array.from(new Set(
|
||||
Object.values(toolDefinitions).flatMap(tool => tool.endpoints || [])
|
||||
Object.values(toolEndpoints).flat() as string[]
|
||||
));
|
||||
const { endpointStatus, loading: endpointsLoading } = useMultipleEndpointsEnabled(allEndpoints);
|
||||
|
||||
const isToolAvailable = useCallback((toolKey: string): boolean => {
|
||||
if (endpointsLoading) return true;
|
||||
const tool = toolDefinitions[toolKey];
|
||||
if (!tool?.endpoints) return true;
|
||||
return tool.endpoints.some(endpoint => endpointStatus[endpoint] === true);
|
||||
const endpoints = toolEndpoints[toolKey] || [];
|
||||
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
|
||||
}, [endpointsLoading, endpointStatus]);
|
||||
|
||||
const toolRegistry: ToolRegistry = useMemo(() => {
|
||||
const availableTools: ToolRegistry = {};
|
||||
Object.keys(toolDefinitions).forEach(toolKey => {
|
||||
const availableToolRegistry: ToolRegistry = {};
|
||||
Object.keys(baseToolRegistry).forEach(toolKey => {
|
||||
if (isToolAvailable(toolKey)) {
|
||||
const toolDef = toolDefinitions[toolKey];
|
||||
availableTools[toolKey] = {
|
||||
...toolDef,
|
||||
name: t(`home.${toolKey}.title`, toolKey.charAt(0).toUpperCase() + toolKey.slice(1))
|
||||
const baseTool = baseToolRegistry[toolKey as keyof typeof baseToolRegistry];
|
||||
availableToolRegistry[toolKey] = {
|
||||
...baseTool,
|
||||
name: t(baseTool.name),
|
||||
description: t(baseTool.description)
|
||||
};
|
||||
}
|
||||
});
|
||||
return availableTools;
|
||||
}, [t, isToolAvailable]);
|
||||
return availableToolRegistry;
|
||||
}, [isToolAvailable, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!endpointsLoading && selectedToolKey && !toolRegistry[selectedToolKey]) {
|
||||
|
@ -104,10 +104,10 @@ function HomePageContent() {
|
||||
|
||||
{/* Left: Tool Picker or Selected Tool Panel */}
|
||||
<div
|
||||
className={`h-screen flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${isRainbowMode ? rainbowStyles.rainbowPaper : ''}`}
|
||||
className={`h-screen flex flex-col overflow-hidden border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${isRainbowMode ? rainbowStyles.rainbowPaper : ''}`}
|
||||
style={{
|
||||
width: sidebarsVisible && !readerMode ? '14vw' : '0',
|
||||
padding: sidebarsVisible && !readerMode ? '0.5rem' : '0'
|
||||
width: sidebarsVisible && !readerMode ? '280px' : '0',
|
||||
backgroundColor: 'var(--bg-toolbar)'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@ -132,7 +132,7 @@ function HomePageContent() {
|
||||
// Selected Tool Content View
|
||||
<div className="flex-1 flex flex-col">
|
||||
{/* Back button */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-4" style={{ padding: '0 1rem' }}>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
@ -144,12 +144,12 @@ function HomePageContent() {
|
||||
</div>
|
||||
|
||||
{/* Tool title */}
|
||||
<div className="mb-4" style={{ marginLeft: '0.5rem' }}>
|
||||
<div className="mb-4" style={{ marginLeft: '1rem' }}>
|
||||
<h2 className="text-lg font-semibold">{selectedTool?.name}</h2>
|
||||
</div>
|
||||
|
||||
{/* Tool content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<div className="flex-1 min-h-0" style={{ padding: '0 1rem' }}>
|
||||
<ToolRenderer
|
||||
selectedToolKey={selectedToolKey}
|
||||
onPreviewFile={setPreviewFile}
|
||||
|
@ -73,12 +73,12 @@
|
||||
--bg-raised: #f9fafb;
|
||||
--bg-muted: #f3f4f6;
|
||||
--bg-background: #f9fafb;
|
||||
--bg-toolbar: #ffffff;
|
||||
--bg-toolbar: #FFFFFF;
|
||||
--text-primary: #111827;
|
||||
--text-secondary: #4b5563;
|
||||
--text-muted: #6b7280;
|
||||
--border-subtle: #e5e7eb;
|
||||
--border-default: #d1d5db;
|
||||
--border-default: #E2E8F0;
|
||||
--border-strong: #9ca3af;
|
||||
--hover-bg: #f9fafb;
|
||||
--active-bg: #f3f4f6;
|
||||
@ -143,12 +143,12 @@
|
||||
--bg-raised: #1F2329;
|
||||
--bg-muted: #1F2329;
|
||||
--bg-background: #2A2F36;
|
||||
--bg-toolbar: #272A2E;
|
||||
--bg-toolbar: #1F2329;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--border-subtle: #2A2F36;
|
||||
--border-default: #374151;
|
||||
--border-default: #3A4047;
|
||||
--border-strong: #4b5563;
|
||||
--hover-bg: #374151;
|
||||
--active-bg: #4b5563;
|
||||
|
Loading…
x
Reference in New Issue
Block a user