diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index db5779a2a..2988799f7 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -11,7 +11,6 @@ import { fileStorage } from '../../services/fileStorage'; import { generateThumbnailForFile } from '../../utils/thumbnailUtils'; import styles from '../pageEditor/PageEditor.module.css'; import FileThumbnail from '../pageEditor/FileThumbnail'; -import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel'; import DragDropGrid from '../pageEditor/DragDropGrid'; import FilePickerModal from '../shared/FilePickerModal'; import SkeletonLoader from '../shared/SkeletonLoader'; @@ -29,11 +28,21 @@ interface FileItem { interface FileEditorProps { onOpenPageEditor?: (file: File) => void; onMergeFiles?: (files: File[]) => void; + toolMode?: boolean; + multiSelect?: boolean; + showUpload?: boolean; + showBulkActions?: boolean; + onFileSelect?: (files: File[]) => void; } const FileEditor = ({ onOpenPageEditor, - onMergeFiles + onMergeFiles, + toolMode = false, + multiSelect = true, + showUpload = true, + showBulkActions = true, + onFileSelect }: FileEditorProps) => { const { t } = useTranslation(); @@ -54,8 +63,14 @@ const FileEditor = ({ const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [localLoading, setLocalLoading] = useState(false); - const [csvInput, setCsvInput] = useState(''); - const [selectionMode, setSelectionMode] = useState(false); + const [selectionMode, setSelectionMode] = useState(toolMode); + + // Enable selection mode automatically in tool mode + React.useEffect(() => { + if (toolMode) { + setSelectionMode(true); + } + }, [toolMode]); const [draggedFile, setDraggedFile] = useState(null); const [dropTarget, setDropTarget] = useState(null); const [multiFileDrag, setMultiFileDrag] = useState<{fileIds: string[], count: number} | null>(null); @@ -183,53 +198,52 @@ const FileEditor = ({ const toggleFile = useCallback((fileId: string) => { const fileName = files.find(f => f.id === fileId)?.name || fileId; - setContextSelectedFiles(prev => - prev.includes(fileName) - ? prev.filter(id => id !== fileName) - : [...prev, fileName] - ); - }, [files, setContextSelectedFiles]); + + if (!multiSelect) { + // Single select mode for tools - toggle on/off + const isCurrentlySelected = selectedFileIds.includes(fileName); + if (isCurrentlySelected) { + // Deselect the file + setContextSelectedFiles([]); + if (onFileSelect) { + onFileSelect([]); + } + } else { + // Select the file + setContextSelectedFiles([fileName]); + const selectedFile = files.find(f => f.id === fileId)?.file; + if (selectedFile && onFileSelect) { + onFileSelect([selectedFile]); + } + } + } else { + // Multi select mode (default) + setContextSelectedFiles(prev => + prev.includes(fileName) + ? prev.filter(id => id !== fileName) + : [...prev, fileName] + ); + + // Notify parent with selected files + if (onFileSelect) { + const selectedFiles = files + .filter(f => selectedFileIds.includes(f.name) || f.name === fileName) + .map(f => f.file); + onFileSelect(selectedFiles); + } + } + }, [files, setContextSelectedFiles, multiSelect, onFileSelect, selectedFileIds]); const toggleSelectionMode = useCallback(() => { setSelectionMode(prev => { const newMode = !prev; if (!newMode) { setContextSelectedFiles([]); - setCsvInput(''); } return newMode; }); }, [setContextSelectedFiles]); - const parseCSVInput = useCallback((csv: string) => { - const fileNames: string[] = []; - const ranges = csv.split(',').map(s => s.trim()).filter(Boolean); - - ranges.forEach(range => { - if (range.includes('-')) { - const [start, end] = range.split('-').map(n => parseInt(n.trim())); - for (let i = start; i <= end && i <= files.length; i++) { - if (i > 0) { - const file = files[i - 1]; - if (file) fileNames.push(file.name); - } - } - } else { - const fileIndex = parseInt(range); - if (fileIndex > 0 && fileIndex <= files.length) { - const file = files[fileIndex - 1]; - if (file) fileNames.push(file.name); - } - } - }); - - return fileNames; - }, [files]); - - const updateFilesFromCSV = useCallback(() => { - const fileNames = parseCSVInput(csvInput); - setContextSelectedFiles(fileNames); - }, [csvInput, parseCSVInput, setContextSelectedFiles]); // Drag and drop handlers const handleDragStart = useCallback((fileId: string) => { @@ -401,22 +415,7 @@ const FileEditor = ({ - - {selectionMode && ( + {showBulkActions && !toolMode && ( <> @@ -424,35 +423,31 @@ const FileEditor = ({ )} {/* Load from storage and upload buttons */} - + {showUpload && ( + <> + - - - + + + + + )} - {selectionMode && ( - - )} {files.length === 0 && !localLoading ? (
@@ -530,6 +525,7 @@ const FileEditor = ({ onMergeFromHere={handleMergeFromHere} onSplitFile={handleSplitFile} onSetStatus={setStatus} + toolMode={toolMode} /> )} renderSplitMarker={(file, index) => ( diff --git a/frontend/src/components/pageEditor/FileThumbnail.tsx b/frontend/src/components/pageEditor/FileThumbnail.tsx index 46448a34c..eff36f1f9 100644 --- a/frontend/src/components/pageEditor/FileThumbnail.tsx +++ b/frontend/src/components/pageEditor/FileThumbnail.tsx @@ -38,6 +38,7 @@ interface FileThumbnailProps { onMergeFromHere: (fileId: string) => void; onSplitFile: (fileId: string) => void; onSetStatus: (status: string) => void; + toolMode?: boolean; } const FileThumbnail = ({ @@ -62,6 +63,7 @@ const FileThumbnail = ({ onMergeFromHere, onSplitFile, onSetStatus, + toolMode = false, }: FileThumbnailProps) => { const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 B'; @@ -238,50 +240,54 @@ const FileThumbnail = ({ whiteSpace: 'nowrap' }} > - - { - e.stopPropagation(); - onViewFile(file.id); - onSetStatus(`Opened ${file.name}`); - }} - > - - - + {!toolMode && ( + <> + + { + e.stopPropagation(); + onViewFile(file.id); + onSetStatus(`Opened ${file.name}`); + }} + > + + + - - { - e.stopPropagation(); - onMergeFromHere(file.id); - onSetStatus(`Starting merge from ${file.name}`); - }} - > - - - + + { + e.stopPropagation(); + onMergeFromHere(file.id); + onSetStatus(`Starting merge from ${file.name}`); + }} + > + + + - - { - e.stopPropagation(); - onSplitFile(file.id); - onSetStatus(`Opening ${file.name} in page editor`); - }} - > - - - + + { + e.stopPropagation(); + onSplitFile(file.id); + onSetStatus(`Opening ${file.name} in page editor`); + }} + > + + + + + )} void; toolParams: any; updateParams: (params: any) => void; + toolSelectedFiles?: File[]; } const ToolRenderer = ({ @@ -21,6 +22,7 @@ const ToolRenderer = ({ setDownloadUrl, toolParams, updateParams, + toolSelectedFiles = [], }: ToolRendererProps) => { if (!selectedTool || !selectedTool.component) { return
Tool not found
; @@ -33,11 +35,9 @@ const ToolRenderer = ({ case "split": return ( ); case "compress": diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 8ad6d5989..ad55394e8 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -36,7 +36,7 @@ type ToolRegistry = { // Base tool registry without translations const baseToolRegistry = { - split: { icon: , component: SplitPdfPanel, view: "viewer" }, + split: { icon: , component: SplitPdfPanel, view: "split" }, compress: { icon: , component: CompressPdfPanel, view: "viewer" }, merge: { icon: , component: MergePdfPanel, view: "pageEditor" }, }; @@ -60,6 +60,8 @@ export default function HomePage() { 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>({}); // Tool registry const toolRegistry: ToolRegistry = { @@ -68,40 +70,57 @@ export default function HomePage() { merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") }, }; - // Tool parameters (simplified for now) + // Tool parameters with state management const getToolParams = (toolKey: string | null) => { if (!toolKey) return {}; - switch (toolKey) { - case 'split': - return { - mode: 'grid', - pages: '', - hDiv: 2, - vDiv: 2, - merge: false, - splitType: 'pages', - splitValue: 1, - bookmarkLevel: 1, - includeMetadata: true, - allowDuplicates: false - }; - case 'compress': - return { - quality: 80, - imageCompression: true, - removeMetadata: false - }; - case 'merge': - return { - sortOrder: 'name', - includeMetadata: true - }; - default: - 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 => ({ @@ -364,7 +383,8 @@ export default function HomePage() { downloadUrl={downloadUrl} setDownloadUrl={setDownloadUrl} toolParams={getToolParams(selectedToolKey)} - updateParams={() => {}} + updateParams={(newParams) => updateToolParams(selectedToolKey, newParams)} + toolSelectedFiles={toolSelectedFiles} /> @@ -461,6 +481,27 @@ export default function HomePage() { /> )} + ) : currentView === "split" ? ( + { + setToolSelectedFiles(files); + }} + /> + ) : selectedToolKey && selectedTool ? ( + {}} + /> ) : ( void; params: { mode: string; pages: string; @@ -31,20 +32,22 @@ export interface SplitPdfPanelProps { allowDuplicates: boolean; }; updateParams: (newParams: Partial) => void; + selectedFiles?: File[]; } const SplitPdfPanel: React.FC = ({ - file, - downloadUrl, - setDownloadUrl, params, updateParams, + selectedFiles = [], }) => { const { t } = useTranslation(); + const fileContext = useFileContext(); + const { activeFiles, selectedFileIds, updateProcessedFile } = fileContext; const [status, setStatus] = useState(""); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); + const [downloadUrl, setDownloadUrl] = useState(null); const { mode, @@ -59,30 +62,30 @@ const SplitPdfPanel: React.FC = ({ allowDuplicates, } = params; + // Clear download when parameters or files change + React.useEffect(() => { + if (downloadUrl) { + setDownloadUrl(null); + setStatus(""); + } + // Reset step 2 completion when parameters change (but not when just status/loading changes) + setStep2Completed(false); + }, [mode, pages, hDiv, vDiv, merge, splitType, splitValue, bookmarkLevel, includeMetadata, allowDuplicates, selectedFiles]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!file) { + if (selectedFiles.length === 0) { setStatus(t("noFileSelected")); return; } const formData = new FormData(); - // Handle IndexedDB files - if (!file.file.id) { - setStatus(t("noFileSelected")); - return; - } - const storedFile = await fileStorage.getFile(file.file.id); - if (storedFile) { - const blob = new Blob([storedFile.data], { type: storedFile.type }); - const actualFile = new File([blob], storedFile.name, { - type: storedFile.type, - lastModified: storedFile.lastModified - }); - formData.append("fileInput", actualFile); - } + // Use selected files from context + selectedFiles.forEach(file => { + formData.append("fileInput", file); + }); let endpoint = ""; @@ -140,126 +143,232 @@ const SplitPdfPanel: React.FC = ({ } }; + // Check if current mode needs additional parameters + const modeNeedsParams = (currentMode: string) => { + return currentMode && currentMode !== ""; // All modes need some params + }; + + // Step 2 completion state + const [step2Completed, setStep2Completed] = useState(false); + + // Check if step 2 settings are valid (for enabling Done button) + const step2SettingsValid = (() => { + if (!mode) return false; + + switch (mode) { + case "byPages": + return pages.trim() !== ""; + case "bySections": + return hDiv !== "" && vDiv !== ""; + case "bySizeOrCount": + return splitType !== "" && splitValue.trim() !== ""; + case "byChapters": + return bookmarkLevel !== ""; + default: + return false; + } + })(); + + // Determine what steps to show + const showStep1 = true; // Always show - Files + const showStep2 = selectedFiles.length > 0; // Settings (mode + params) + const showStep3 = step2Completed; // Review (apply & continue vs export) + + // Determine if steps are collapsed (completed) + const step1Collapsed = selectedFiles.length > 0; + const step2Collapsed = step2Completed; + return ( -
- - v && updateParams({ mode: 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") }, + { value: "bySizeOrCount", label: t("split-by-size-or-count.title", "Split by Size or Count") }, + { value: "byChapters", label: t("splitByChapters.title", "Split by Chapters") }, + ]} + /> - {mode === "bySections" && ( - - updateParams({ hDiv: e.target.value })} - placeholder={t("split-by-sections.horizontal.placeholder", "Enter number of horizontal divisions")} - /> - updateParams({ vDiv: e.target.value })} - placeholder={t("split-by-sections.vertical.placeholder", "Enter number of vertical divisions")} - /> - updateParams({ merge: e.currentTarget.checked })} - /> - - )} + {/* Mode-specific Parameters */} + {mode === "byPages" && ( + updateParams({ pages: e.target.value })} + /> + )} - {mode === "bySizeOrCount" && ( - - v && updateParams({ splitType: v })} + data={[ + { value: "size", label: t("split-by-size-or-count.type.size", "By Size") }, + { value: "pages", label: t("split-by-size-or-count.type.pageCount", "By Page Count") }, + { value: "docs", label: t("split-by-size-or-count.type.docCount", "By Document Count") }, + ]} + /> + updateParams({ splitValue: e.target.value })} + /> + + )} - + {mode === "byChapters" && ( + + updateParams({ bookmarkLevel: e.target.value })} + /> + updateParams({ includeMetadata: e.currentTarget.checked })} + /> + updateParams({ allowDuplicates: e.currentTarget.checked })} + /> + + )} - {status &&

{status}

} + {/* Done Button */} + {mode && ( + + )} +
+ )} + + )} - {errorMessage && ( - setErrorMessage(null)}> - {errorMessage} - - )} + {/* Step 3: Review */} + {showStep3 && ( + + 3. Review + + + - {status === t("downloadComplete") && downloadUrl && ( - - )} - - + {status && {status}} + + {errorMessage && ( + setErrorMessage(null)} mt="sm"> + {errorMessage} + + )} + + {downloadUrl && ( + + )} + +
+ )} + + ); };