diff --git a/frontend/src/components/tools/ToolRenderer.tsx b/frontend/src/components/tools/ToolRenderer.tsx index c8c448c04..4a9b9901d 100644 --- a/frontend/src/components/tools/ToolRenderer.tsx +++ b/frontend/src/components/tools/ToolRenderer.tsx @@ -1,13 +1,10 @@ -import React from "react"; import { FileWithUrl } from "../../types/file"; +import { useToolManagement } from "../../hooks/useToolManagement"; interface ToolRendererProps { selectedToolKey: string; - selectedTool: any; pdfFile: any; files: FileWithUrl[]; - downloadUrl: string | null; - setDownloadUrl: (url: string | null) => void; toolParams: any; updateParams: (params: any) => void; toolSelectedFiles?: File[]; @@ -16,18 +13,18 @@ interface ToolRendererProps { const ToolRenderer = ({ selectedToolKey, - selectedTool, - pdfFile, - files, - downloadUrl, - setDownloadUrl, +files, toolParams, updateParams, toolSelectedFiles = [], onPreviewFile, }: ToolRendererProps) => { + // Get the tool from registry + const { toolRegistry } = useToolManagement(); + const selectedTool = toolRegistry[selectedToolKey]; + if (!selectedTool || !selectedTool.component) { - return
Tool not found
; + return
Tool not found: {selectedToolKey}
; } const ToolComponent = selectedTool.component; @@ -37,8 +34,6 @@ const ToolRenderer = ({ case "split": return ( @@ -47,7 +42,6 @@ const ToolRenderer = ({ return ( {}} params={toolParams} updateParams={updateParams} @@ -57,7 +51,6 @@ const ToolRenderer = ({ return ( @@ -66,7 +59,6 @@ const ToolRenderer = ({ return ( @@ -74,4 +66,4 @@ const ToolRenderer = ({ } }; -export default ToolRenderer; \ No newline at end of file +export default ToolRenderer; diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx index 9cfc49e20..e3b0eb23e 100644 --- a/frontend/src/components/viewer/Viewer.tsx +++ b/frontend/src/components/viewer/Viewer.tsx @@ -13,6 +13,8 @@ import CloseIcon from "@mui/icons-material/Close"; import { useLocalStorage } from "@mantine/hooks"; import { fileStorage } from "../../services/fileStorage"; import SkeletonLoader from '../shared/SkeletonLoader'; +import { useFileContext } from "../../contexts/FileContext"; +import { useFileWithUrl } from "../../hooks/useFileWithUrl"; GlobalWorkerOptions.workerSrc = "/pdf.worker.js"; @@ -132,8 +134,6 @@ const LazyPageImage = ({ }; export interface ViewerProps { - pdfFile: { file: File; url: string } | null; // First file in the array - setPdfFile: (file: { file: File; url: string } | null) => void; sidebarsVisible: boolean; setSidebarsVisible: (v: boolean) => void; onClose?: () => void; @@ -141,8 +141,6 @@ export interface ViewerProps { } const Viewer = ({ - pdfFile, - setPdfFile, sidebarsVisible, setSidebarsVisible, onClose, @@ -150,6 +148,14 @@ const Viewer = ({ }: ViewerProps) => { const { t } = useTranslation(); const theme = useMantineTheme(); + + // Get current file from FileContext + const { getCurrentFile, getCurrentProcessedFile, clearAllFiles, addFiles } = useFileContext(); + const currentFile = getCurrentFile(); + const processedFile = getCurrentProcessedFile(); + + // Convert File to FileWithUrl format for viewer + const pdfFile = useFileWithUrl(currentFile); const [numPages, setNumPages] = useState(0); const [pageImages, setPageImages] = useState([]); const [loading, setLoading] = useState(false); @@ -448,8 +454,7 @@ const Viewer = ({ onChange={(e) => { const file = e.target.files?.[0]; if (file && file.type === "application/pdf") { - const fileUrl = URL.createObjectURL(file); - setPdfFile({ file, url: fileUrl }); + addFiles([file]); } }} /> diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx new file mode 100644 index 000000000..b74e08353 --- /dev/null +++ b/frontend/src/hooks/useToolManagement.tsx @@ -0,0 +1,129 @@ +import React, { useState, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import AddToPhotosIcon from "@mui/icons-material/AddToPhotos"; +import ContentCutIcon from "@mui/icons-material/ContentCut"; +import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; +import SplitPdfPanel from "../tools/Split"; +import CompressPdfPanel from "../tools/Compress"; +import MergePdfPanel from "../tools/Merge"; + +type ToolRegistryEntry = { + icon: React.ReactNode; + name: string; + component: React.ComponentType; + view: string; +}; + +type ToolRegistry = { + [key: string]: ToolRegistryEntry; +}; + +// Base tool registry without translations +const baseToolRegistry = { + split: { icon: , component: SplitPdfPanel, view: "split" }, + compress: { icon: , component: CompressPdfPanel, view: "viewer" }, + merge: { icon: , component: MergePdfPanel, view: "pageEditor" }, +}; + +// Tool parameter defaults +const getToolDefaults = (toolKey: string) => { + switch (toolKey) { + case 'split': + return { + mode: '', + pages: '', + hDiv: '2', + vDiv: '2', + merge: false, + splitType: 'size', + splitValue: '', + bookmarkLevel: '1', + includeMetadata: false, + allowDuplicates: false, + }; + case 'compress': + return { + quality: 80, + imageCompression: true, + removeMetadata: false + }; + case 'merge': + return { + sortOrder: 'name', + includeMetadata: true + }; + default: + return {}; + } +}; + +export const useToolManagement = () => { + const { t } = useTranslation(); + + const [selectedToolKey, setSelectedToolKey] = useState(null); + const [toolSelectedFileIds, setToolSelectedFileIds] = useState([]); + const [toolParams, setToolParams] = useState>({}); + + // Tool registry with translations + const toolRegistry: ToolRegistry = useMemo(() => ({ + split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") }, + compress: { ...baseToolRegistry.compress, name: t("home.compressPdfs.title", "Compress PDF") }, + merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") }, + }), [t]); + + // Get tool parameters with defaults + const getToolParams = useCallback((toolKey: string | null) => { + if (!toolKey) return {}; + + const storedParams = toolParams[toolKey] || {}; + const defaultParams = getToolDefaults(toolKey); + + return { ...defaultParams, ...storedParams }; + }, [toolParams]); + + // Update tool parameters + const updateToolParams = useCallback((toolKey: string, newParams: any) => { + setToolParams(prev => ({ + ...prev, + [toolKey]: { + ...prev[toolKey], + ...newParams + } + })); + }, []); + + // Select tool + const selectTool = useCallback((toolKey: string) => { + setSelectedToolKey(toolKey); + }, []); + + // Clear tool selection + const clearToolSelection = useCallback(() => { + setSelectedToolKey(null); + }, []); + + // Get currently selected tool + const selectedTool = selectedToolKey ? toolRegistry[selectedToolKey] : null; + + return { + // State + selectedToolKey, + selectedTool, + toolSelectedFileIds, + toolParams: getToolParams(selectedToolKey), + toolRegistry, + + // Actions + selectTool, + clearToolSelection, + updateToolParams: (newParams: any) => { + if (selectedToolKey) { + updateToolParams(selectedToolKey, newParams); + } + }, + setToolSelectedFileIds, + + // Utilities + getToolParams, + }; +}; \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 36a853134..b5475db56 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,12 +1,8 @@ -import React, { useState, useCallback, useEffect } from "react"; +import React, { useState, useCallback} from "react"; import { useTranslation } from 'react-i18next'; -import { useFileWithUrl } from "../hooks/useFileWithUrl"; import { useFileContext } from "../contexts/FileContext"; -import { fileStorage } from "../services/fileStorage"; -import AddToPhotosIcon from "@mui/icons-material/AddToPhotos"; -import ContentCutIcon from "@mui/icons-material/ContentCut"; -import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; -import { Group, Paper, Box, Button, useMantineTheme, Container } from "@mantine/core"; +import { useToolManagement } from "../hooks/useToolManagement"; +import { Group, Box, Button, Container } from "@mantine/core"; import { useRainbowThemeContext } from "../components/shared/RainbowThemeProvider"; import rainbowStyles from '../styles/rainbow.module.css'; @@ -17,163 +13,50 @@ import PageEditor from "../components/pageEditor/PageEditor"; import PageEditorControls from "../components/pageEditor/PageEditorControls"; import Viewer from "../components/viewer/Viewer"; import FileUploadSelector from "../components/shared/FileUploadSelector"; -import SplitPdfPanel from "../tools/Split"; -import CompressPdfPanel from "../tools/Compress"; -import MergePdfPanel from "../tools/Merge"; import ToolRenderer from "../components/tools/ToolRenderer"; import QuickAccessBar from "../components/shared/QuickAccessBar"; -type ToolRegistryEntry = { - icon: React.ReactNode; - name: string; - component: React.ComponentType; - view: string; -}; - -type ToolRegistry = { - [key: string]: ToolRegistryEntry; -}; - -// Base tool registry without translations -const baseToolRegistry = { - split: { icon: , component: SplitPdfPanel, view: "split" }, - compress: { icon: , component: CompressPdfPanel, view: "viewer" }, - merge: { icon: , component: MergePdfPanel, view: "pageEditor" }, -}; - export default function HomePage() { const { t } = useTranslation(); - const theme = useMantineTheme(); const { isRainbowMode } = useRainbowThemeContext(); - + // Get file context const fileContext = useFileContext(); const { activeFiles, currentView, currentMode, setCurrentView, addFiles } = fileContext; - // Core app state - const [selectedToolKey, setSelectedToolKey] = useState(null); + const { + selectedToolKey, + selectedTool, + toolParams, + toolRegistry, + selectTool, + clearToolSelection, + updateToolParams, + } = useToolManagement(); - const [storedFiles, setStoredFiles] = useState([]); - const [preSelectedFiles, setPreSelectedFiles] = useState([]); - const [downloadUrl, setDownloadUrl] = useState(null); + const [toolSelectedFiles, setToolSelectedFiles] = useState([]); const [sidebarsVisible, setSidebarsVisible] = useState(true); const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker'); const [readerMode, setReaderMode] = useState(false); const [pageEditorFunctions, setPageEditorFunctions] = useState(null); - const [toolSelectedFiles, setToolSelectedFiles] = useState([]); - const [toolParams, setToolParams] = useState>({}); const [previewFile, setPreviewFile] = useState(null); - // Tool registry - const toolRegistry: ToolRegistry = { - split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") }, - compress: { ...baseToolRegistry.compress, name: t("home.compressPdfs.title", "Compress PDF") }, - merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") }, - }; - - // Tool parameters with state management - const getToolParams = (toolKey: string | null) => { - if (!toolKey) return {}; - - // Get stored params for this tool, or use defaults - const storedParams = toolParams[toolKey] || {}; - - const defaultParams = (() => { - switch (toolKey) { - case 'split': - return { - mode: '', - pages: '', - hDiv: '2', - vDiv: '2', - merge: false, - splitType: 'size', - splitValue: '', - bookmarkLevel: '1', - includeMetadata: false, - allowDuplicates: false, - }; - case 'compress': - return { - quality: 80, - imageCompression: true, - removeMetadata: false - }; - case 'merge': - return { - sortOrder: 'name', - includeMetadata: true - }; - default: - return {}; - } - })(); - - return { ...defaultParams, ...storedParams }; - }; - - const updateToolParams = useCallback((toolKey: string, newParams: any) => { - setToolParams(prev => ({ - ...prev, - [toolKey]: { - ...prev[toolKey], - ...newParams - } - })); - }, []); - - - useEffect(() => { - const activeFileData = activeFiles.map(file => ({ - name: file.name, - size: file.size, - type: file.type, - lastModified: file.lastModified - })); - localStorage.setItem('activeFiles', JSON.stringify(activeFileData)); - }, [activeFiles]); - - useEffect(() => { - const loadStoredFiles = async () => { - try { - const files = await fileStorage.getAllFiles(); - setStoredFiles(files); - } catch (error) { - console.warn('Failed to load stored files:', error); - } - }; - loadStoredFiles(); - }, []); - - useEffect(() => { - const restoreActiveFiles = async () => { - try { - const savedFileData = JSON.parse(localStorage.getItem('activeFiles') || '[]'); - if (savedFileData.length > 0) { - // File restoration handled by FileContext - } - } catch (error) { - console.warn('Failed to restore active files:', error); - } - }; - restoreActiveFiles(); - }, []); const handleToolSelect = useCallback( (id: string) => { - setSelectedToolKey(id); + selectTool(id); if (toolRegistry[id]?.view) setCurrentView(toolRegistry[id].view); setLeftPanelView('toolContent'); setReaderMode(false); }, - [toolRegistry, setCurrentView] + [selectTool, toolRegistry, setCurrentView] ); const handleQuickAccessTools = useCallback(() => { setLeftPanelView('toolPicker'); setReaderMode(false); - setSelectedToolKey(null); - }, []); + clearToolSelection(); + }, [clearToolSelection]); const handleReaderToggle = useCallback(() => { setReaderMode(!readerMode); @@ -190,123 +73,7 @@ export default function HomePage() { } }, [activeFiles, addFiles]); - const removeFromActiveFiles = useCallback((file: File) => { - fileContext.removeFiles([file.name]); - }, [fileContext]); - const setCurrentActiveFile = useCallback(async (file: File) => { - const filtered = activeFiles.filter(f => !(f.name === file.name && f.size === file.size)); - await addFiles([file, ...filtered]); - }, [activeFiles, addFiles]); - - const handleFileSelect = useCallback((file: File) => { - addToActiveFiles(file); - }, [addToActiveFiles]); - - // Handle opening file editor with selected files - const handleOpenFileEditor = useCallback(async (selectedFiles) => { - if (!selectedFiles || selectedFiles.length === 0) { - setPreSelectedFiles([]); - handleViewChange("fileEditor"); - return; - } - - // Convert FileWithUrl[] to File[] and add to activeFiles - try { - const convertedFiles = await Promise.all( - selectedFiles.map(async (fileItem) => { - // If it's already a File, return as is - if (fileItem instanceof File) { - return fileItem; - } - - // If it has a file property, use that - if (fileItem.file && fileItem.file instanceof File) { - return fileItem.file; - } - - // If it's from IndexedDB storage, reconstruct the File - if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') { - const arrayBuffer = await fileItem.arrayBuffer(); - const blob = new Blob([arrayBuffer], { type: fileItem.type || 'application/pdf' }); - const file = new File([blob], fileItem.name, { - type: fileItem.type || 'application/pdf', - lastModified: fileItem.lastModified || Date.now() - }); - // Mark as from storage to avoid re-storing - (file as any).storedInIndexedDB = true; - return file; - } - - console.warn('Could not convert file item:', fileItem); - return null; - }) - ); - - // Filter out nulls and add to activeFiles - const validFiles = convertedFiles.filter((f): f is File => f !== null); - await addFiles(validFiles); - setPreSelectedFiles([]); // Clear preselected since we're using activeFiles now - handleViewChange("fileEditor"); - } catch (error) { - console.error('Error converting selected files:', error); - } - }, [handleViewChange, addFiles]); - - // Handle opening page editor with selected files - const handleOpenPageEditor = useCallback(async (selectedFiles) => { - if (!selectedFiles || selectedFiles.length === 0) { - handleViewChange("pageEditor"); - return; - } - - // Convert FileWithUrl[] to File[] and add to activeFiles - try { - const convertedFiles = await Promise.all( - selectedFiles.map(async (fileItem) => { - // If it's already a File, return as is - if (fileItem instanceof File) { - return fileItem; - } - - // If it has a file property, use that - if (fileItem.file && fileItem.file instanceof File) { - return fileItem.file; - } - - // If it's from IndexedDB storage, reconstruct the File - if (fileItem.arrayBuffer && typeof fileItem.arrayBuffer === 'function') { - const arrayBuffer = await fileItem.arrayBuffer(); - const blob = new Blob([arrayBuffer], { type: fileItem.type || 'application/pdf' }); - const file = new File([blob], fileItem.name, { - type: fileItem.type || 'application/pdf', - lastModified: fileItem.lastModified || Date.now() - }); - // Mark as from storage to avoid re-storing - (file as any).storedInIndexedDB = true; - return file; - } - - console.warn('Could not convert file item:', fileItem); - return null; - }) - ); - - // Filter out nulls and add to activeFiles - const validFiles = convertedFiles.filter((f): f is File => f !== null); - await addFiles(validFiles); - handleViewChange("pageEditor"); - } catch (error) { - console.error('Error converting selected files for page editor:', error); - } - }, [handleViewChange, addFiles]); - - const selectedTool = toolRegistry[selectedToolKey]; - - // For Viewer - convert first active file to expected format (only when needed) - const currentFileWithUrl = useFileWithUrl( - (currentView === "viewer" && activeFiles[0]) ? activeFiles[0] : null - ); return ( - {/* Left: Tool Picker OR Selected Tool Panel */} + {/* Left: Tool Picker or Selected Tool Panel */}
updateToolParams(selectedToolKey, newParams)} toolSelectedFiles={toolSelectedFiles} onPreviewFile={setPreviewFile} /> @@ -409,7 +164,7 @@ export default function HomePage() { selectedToolKey={selectedToolKey} /> {/* Main content area */} - { addToActiveFiles(file); }} @@ -439,7 +193,6 @@ export default function HomePage() { ) : currentView === "fileEditor" ? ( { - setCurrentActiveFile(file); handleViewChange("pageEditor"); }} onMergeFiles={(filesToMerge) => { @@ -450,14 +203,6 @@ export default function HomePage() { /> ) : currentView === "viewer" ? ( { - if (fileObj) { - setCurrentActiveFile(fileObj.file); - } else { - fileContext.clearAllFiles(); - } - }} sidebarsVisible={sidebarsVisible} setSidebarsVisible={setSidebarsVisible} previewFile={previewFile} @@ -466,7 +211,7 @@ export default function HomePage() { setPreviewFile(null); // Clear preview file const previousMode = sessionStorage.getItem('previousMode'); if (previousMode === 'split') { - setSelectedToolKey('split'); + selectTool('split'); setCurrentView('split'); setLeftPanelView('toolContent'); sessionStorage.removeItem('previousMode'); @@ -512,20 +257,12 @@ export default function HomePage() { ) : selectedToolKey && selectedTool ? ( {}} /> ) : ( { addToActiveFiles(file); }} diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index c06451800..0a03c2794 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -26,26 +26,11 @@ import { generateThumbnailForFile } from "../utils/thumbnailUtils"; import FileEditor from "../components/fileEditor/FileEditor"; export interface SplitPdfPanelProps { - params: { - mode: string; - pages: string; - hDiv: string; - vDiv: string; - merge: boolean; - splitType: string; - splitValue: string; - bookmarkLevel: string; - includeMetadata: boolean; - allowDuplicates: boolean; - }; - updateParams: (newParams: Partial) => void; selectedFiles?: File[]; onPreviewFile?: (file: File | null) => void; } const SplitPdfPanel: React.FC = ({ - params, - updateParams, selectedFiles = [], onPreviewFile, }) => { @@ -53,6 +38,18 @@ const SplitPdfPanel: React.FC = ({ const fileContext = useFileContext(); const { activeFiles, selectedFileIds, updateProcessedFile, recordOperation, markOperationApplied, markOperationFailed, setCurrentMode } = fileContext; + // Internal split parameter state + const [mode, setMode] = useState(''); + const [pages, setPages] = useState(''); + const [hDiv, setHDiv] = useState('2'); + const [vDiv, setVDiv] = useState('2'); + const [merge, setMerge] = useState(false); + const [splitType, setSplitType] = useState('size'); + const [splitValue, setSplitValue] = useState(''); + const [bookmarkLevel, setBookmarkLevel] = useState('1'); + const [includeMetadata, setIncludeMetadata] = useState(false); + const [allowDuplicates, setAllowDuplicates] = useState(false); + const [status, setStatus] = useState(""); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); @@ -67,19 +64,6 @@ const SplitPdfPanel: React.FC = ({ isGeneratingThumbnails: false }); - const { - mode, - pages, - hDiv, - vDiv, - merge, - splitType, - splitValue, - bookmarkLevel, - includeMetadata, - allowDuplicates, - } = params; - // Clear download when parameters or files change React.useEffect(() => { if (downloadUrl) { @@ -339,7 +323,7 @@ const SplitPdfPanel: React.FC = ({ label="Choose split method" placeholder="Select how to split the PDF" value={mode} - onChange={(v) => v && updateParams({ mode: v })} + onChange={(v) => v && setMode(v)} data={[ { value: "byPages", label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" }, { value: "bySections", label: t("split-by-sections.title", "Split by Grid Sections") }, @@ -354,7 +338,7 @@ const SplitPdfPanel: React.FC = ({ label={t("split.splitPages", "Pages")} placeholder={t("pageSelectionPrompt", "e.g. 1,3,5-10")} value={pages} - onChange={(e) => updateParams({ pages: e.target.value })} + onChange={(e) => setPages(e.target.value)} /> )} @@ -366,7 +350,7 @@ const SplitPdfPanel: React.FC = ({ min="0" max="300" value={hDiv} - onChange={(e) => updateParams({ hDiv: e.target.value })} + onChange={(e) => setHDiv(e.target.value)} placeholder={t("split-by-sections.horizontal.placeholder", "Enter number of horizontal divisions")} /> = ({ min="0" max="300" value={vDiv} - onChange={(e) => updateParams({ vDiv: e.target.value })} + onChange={(e) => setVDiv(e.target.value)} placeholder={t("split-by-sections.vertical.placeholder", "Enter number of vertical divisions")} /> updateParams({ merge: e.currentTarget.checked })} + onChange={(e) => setMerge(e.currentTarget.checked)} /> )} @@ -391,7 +375,7 @@ const SplitPdfPanel: React.FC = ({