Split reimplementation

This commit is contained in:
Reece 2025-07-10 01:48:13 +01:00
parent 666c15fabd
commit c7dce8a68f
5 changed files with 444 additions and 292 deletions

View File

@ -11,7 +11,6 @@ import { fileStorage } from '../../services/fileStorage';
import { generateThumbnailForFile } from '../../utils/thumbnailUtils'; import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
import styles from '../pageEditor/PageEditor.module.css'; import styles from '../pageEditor/PageEditor.module.css';
import FileThumbnail from '../pageEditor/FileThumbnail'; import FileThumbnail from '../pageEditor/FileThumbnail';
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
import DragDropGrid from '../pageEditor/DragDropGrid'; import DragDropGrid from '../pageEditor/DragDropGrid';
import FilePickerModal from '../shared/FilePickerModal'; import FilePickerModal from '../shared/FilePickerModal';
import SkeletonLoader from '../shared/SkeletonLoader'; import SkeletonLoader from '../shared/SkeletonLoader';
@ -29,11 +28,21 @@ interface FileItem {
interface FileEditorProps { interface FileEditorProps {
onOpenPageEditor?: (file: File) => void; onOpenPageEditor?: (file: File) => void;
onMergeFiles?: (files: File[]) => void; onMergeFiles?: (files: File[]) => void;
toolMode?: boolean;
multiSelect?: boolean;
showUpload?: boolean;
showBulkActions?: boolean;
onFileSelect?: (files: File[]) => void;
} }
const FileEditor = ({ const FileEditor = ({
onOpenPageEditor, onOpenPageEditor,
onMergeFiles onMergeFiles,
toolMode = false,
multiSelect = true,
showUpload = true,
showBulkActions = true,
onFileSelect
}: FileEditorProps) => { }: FileEditorProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -54,8 +63,14 @@ const FileEditor = ({
const [status, setStatus] = useState<string | null>(null); const [status, setStatus] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [localLoading, setLocalLoading] = useState(false); const [localLoading, setLocalLoading] = useState(false);
const [csvInput, setCsvInput] = useState<string>(''); const [selectionMode, setSelectionMode] = useState(toolMode);
const [selectionMode, setSelectionMode] = useState(false);
// Enable selection mode automatically in tool mode
React.useEffect(() => {
if (toolMode) {
setSelectionMode(true);
}
}, [toolMode]);
const [draggedFile, setDraggedFile] = useState<string | null>(null); const [draggedFile, setDraggedFile] = useState<string | null>(null);
const [dropTarget, setDropTarget] = useState<string | null>(null); const [dropTarget, setDropTarget] = useState<string | null>(null);
const [multiFileDrag, setMultiFileDrag] = useState<{fileIds: string[], count: number} | null>(null); const [multiFileDrag, setMultiFileDrag] = useState<{fileIds: string[], count: number} | null>(null);
@ -183,53 +198,52 @@ const FileEditor = ({
const toggleFile = useCallback((fileId: string) => { const toggleFile = useCallback((fileId: string) => {
const fileName = files.find(f => f.id === fileId)?.name || fileId; const fileName = files.find(f => f.id === fileId)?.name || fileId;
setContextSelectedFiles(prev =>
prev.includes(fileName) if (!multiSelect) {
? prev.filter(id => id !== fileName) // Single select mode for tools - toggle on/off
: [...prev, fileName] const isCurrentlySelected = selectedFileIds.includes(fileName);
); if (isCurrentlySelected) {
}, [files, setContextSelectedFiles]); // 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(() => { const toggleSelectionMode = useCallback(() => {
setSelectionMode(prev => { setSelectionMode(prev => {
const newMode = !prev; const newMode = !prev;
if (!newMode) { if (!newMode) {
setContextSelectedFiles([]); setContextSelectedFiles([]);
setCsvInput('');
} }
return newMode; return newMode;
}); });
}, [setContextSelectedFiles]); }, [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 // Drag and drop handlers
const handleDragStart = useCallback((fileId: string) => { const handleDragStart = useCallback((fileId: string) => {
@ -401,22 +415,7 @@ const FileEditor = ({
<Box p="md" pt="xl"> <Box p="md" pt="xl">
<Group mb="md"> <Group mb="md">
<Button {showBulkActions && !toolMode && (
onClick={toggleSelectionMode}
variant={selectionMode ? "filled" : "outline"}
color={selectionMode ? "blue" : "gray"}
styles={{
root: {
transition: 'all 0.2s ease',
...(selectionMode && {
boxShadow: '0 2px 8px rgba(59, 130, 246, 0.3)',
})
}
}}
>
{selectionMode ? "Exit Selection" : "Select Files"}
</Button>
{selectionMode && (
<> <>
<Button onClick={selectAll} variant="light">Select All</Button> <Button onClick={selectAll} variant="light">Select All</Button>
<Button onClick={deselectAll} variant="light">Deselect All</Button> <Button onClick={deselectAll} variant="light">Deselect All</Button>
@ -424,35 +423,31 @@ const FileEditor = ({
)} )}
{/* Load from storage and upload buttons */} {/* Load from storage and upload buttons */}
<Button {showUpload && (
variant="outline" <>
color="blue" <Button
onClick={() => setShowFilePickerModal(true)} variant="outline"
> color="blue"
Load from Storage onClick={() => setShowFilePickerModal(true)}
</Button> >
Load from Storage
</Button>
<Dropzone <Dropzone
onDrop={handleFileUpload} onDrop={handleFileUpload}
accept={["application/pdf"]} accept={["application/pdf"]}
multiple={true} multiple={true}
maxSize={2 * 1024 * 1024 * 1024} maxSize={2 * 1024 * 1024 * 1024}
style={{ display: 'contents' }} style={{ display: 'contents' }}
> >
<Button variant="outline" color="green"> <Button variant="outline" color="green">
Upload Files Upload Files
</Button> </Button>
</Dropzone> </Dropzone>
</>
)}
</Group> </Group>
{selectionMode && (
<BulkSelectionPanel
csvInput={csvInput}
setCsvInput={setCsvInput}
selectedPages={localSelectedFiles}
onUpdatePagesFromCSV={updateFilesFromCSV}
/>
)}
{files.length === 0 && !localLoading ? ( {files.length === 0 && !localLoading ? (
<Center h="60vh"> <Center h="60vh">
@ -530,6 +525,7 @@ const FileEditor = ({
onMergeFromHere={handleMergeFromHere} onMergeFromHere={handleMergeFromHere}
onSplitFile={handleSplitFile} onSplitFile={handleSplitFile}
onSetStatus={setStatus} onSetStatus={setStatus}
toolMode={toolMode}
/> />
)} )}
renderSplitMarker={(file, index) => ( renderSplitMarker={(file, index) => (

View File

@ -38,6 +38,7 @@ interface FileThumbnailProps {
onMergeFromHere: (fileId: string) => void; onMergeFromHere: (fileId: string) => void;
onSplitFile: (fileId: string) => void; onSplitFile: (fileId: string) => void;
onSetStatus: (status: string) => void; onSetStatus: (status: string) => void;
toolMode?: boolean;
} }
const FileThumbnail = ({ const FileThumbnail = ({
@ -62,6 +63,7 @@ const FileThumbnail = ({
onMergeFromHere, onMergeFromHere,
onSplitFile, onSplitFile,
onSetStatus, onSetStatus,
toolMode = false,
}: FileThumbnailProps) => { }: FileThumbnailProps) => {
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
if (bytes === 0) return '0 B'; if (bytes === 0) return '0 B';
@ -238,50 +240,54 @@ const FileThumbnail = ({
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}} }}
> >
<Tooltip label="View File"> {!toolMode && (
<ActionIcon <>
size="md" <Tooltip label="View File">
variant="subtle" <ActionIcon
c="white" size="md"
onClick={(e) => { variant="subtle"
e.stopPropagation(); c="white"
onViewFile(file.id); onClick={(e) => {
onSetStatus(`Opened ${file.name}`); e.stopPropagation();
}} onViewFile(file.id);
> onSetStatus(`Opened ${file.name}`);
<VisibilityIcon style={{ fontSize: 20 }} /> }}
</ActionIcon> >
</Tooltip> <VisibilityIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Tooltip>
<Tooltip label="Merge from here"> <Tooltip label="Merge from here">
<ActionIcon <ActionIcon
size="md" size="md"
variant="subtle" variant="subtle"
c="white" c="white"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onMergeFromHere(file.id); onMergeFromHere(file.id);
onSetStatus(`Starting merge from ${file.name}`); onSetStatus(`Starting merge from ${file.name}`);
}} }}
> >
<MergeIcon style={{ fontSize: 20 }} /> <MergeIcon style={{ fontSize: 20 }} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="Split File"> <Tooltip label="Split File">
<ActionIcon <ActionIcon
size="md" size="md"
variant="subtle" variant="subtle"
c="white" c="white"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onSplitFile(file.id); onSplitFile(file.id);
onSetStatus(`Opening ${file.name} in page editor`); onSetStatus(`Opening ${file.name} in page editor`);
}} }}
> >
<SplitscreenIcon style={{ fontSize: 20 }} /> <SplitscreenIcon style={{ fontSize: 20 }} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</>
)}
<Tooltip label="Delete File"> <Tooltip label="Delete File">
<ActionIcon <ActionIcon

View File

@ -10,6 +10,7 @@ interface ToolRendererProps {
setDownloadUrl: (url: string | null) => void; setDownloadUrl: (url: string | null) => void;
toolParams: any; toolParams: any;
updateParams: (params: any) => void; updateParams: (params: any) => void;
toolSelectedFiles?: File[];
} }
const ToolRenderer = ({ const ToolRenderer = ({
@ -21,6 +22,7 @@ const ToolRenderer = ({
setDownloadUrl, setDownloadUrl,
toolParams, toolParams,
updateParams, updateParams,
toolSelectedFiles = [],
}: ToolRendererProps) => { }: ToolRendererProps) => {
if (!selectedTool || !selectedTool.component) { if (!selectedTool || !selectedTool.component) {
return <div>Tool not found</div>; return <div>Tool not found</div>;
@ -33,11 +35,9 @@ const ToolRenderer = ({
case "split": case "split":
return ( return (
<ToolComponent <ToolComponent
file={pdfFile}
downloadUrl={downloadUrl}
setDownloadUrl={setDownloadUrl}
params={toolParams} params={toolParams}
updateParams={updateParams} updateParams={updateParams}
selectedFiles={toolSelectedFiles}
/> />
); );
case "compress": case "compress":

View File

@ -36,7 +36,7 @@ type ToolRegistry = {
// Base tool registry without translations // Base tool registry without translations
const baseToolRegistry = { const baseToolRegistry = {
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "viewer" }, split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "split" },
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "viewer" }, compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "viewer" },
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "pageEditor" }, merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "pageEditor" },
}; };
@ -60,6 +60,8 @@ export default function HomePage() {
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker'); const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
const [readerMode, setReaderMode] = useState(false); const [readerMode, setReaderMode] = useState(false);
const [pageEditorFunctions, setPageEditorFunctions] = useState<any>(null); const [pageEditorFunctions, setPageEditorFunctions] = useState<any>(null);
const [toolSelectedFiles, setToolSelectedFiles] = useState<File[]>([]);
const [toolParams, setToolParams] = useState<Record<string, any>>({});
// Tool registry // Tool registry
const toolRegistry: ToolRegistry = { const toolRegistry: ToolRegistry = {
@ -68,40 +70,57 @@ export default function HomePage() {
merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") }, 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) => { const getToolParams = (toolKey: string | null) => {
if (!toolKey) return {}; if (!toolKey) return {};
switch (toolKey) { // Get stored params for this tool, or use defaults
case 'split': const storedParams = toolParams[toolKey] || {};
return {
mode: 'grid', const defaultParams = (() => {
pages: '', switch (toolKey) {
hDiv: 2, case 'split':
vDiv: 2, return {
merge: false, mode: '',
splitType: 'pages', pages: '',
splitValue: 1, hDiv: '2',
bookmarkLevel: 1, vDiv: '2',
includeMetadata: true, merge: false,
allowDuplicates: false splitType: 'size',
}; splitValue: '',
case 'compress': bookmarkLevel: '1',
return { includeMetadata: false,
quality: 80, allowDuplicates: false,
imageCompression: true, };
removeMetadata: false case 'compress':
}; return {
case 'merge': quality: 80,
return { imageCompression: true,
sortOrder: 'name', removeMetadata: false
includeMetadata: true };
}; case 'merge':
default: return {
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(() => { useEffect(() => {
const activeFileData = activeFiles.map(file => ({ const activeFileData = activeFiles.map(file => ({
@ -364,7 +383,8 @@ export default function HomePage() {
downloadUrl={downloadUrl} downloadUrl={downloadUrl}
setDownloadUrl={setDownloadUrl} setDownloadUrl={setDownloadUrl}
toolParams={getToolParams(selectedToolKey)} toolParams={getToolParams(selectedToolKey)}
updateParams={() => {}} updateParams={(newParams) => updateToolParams(selectedToolKey, newParams)}
toolSelectedFiles={toolSelectedFiles}
/> />
</div> </div>
</div> </div>
@ -461,6 +481,27 @@ export default function HomePage() {
/> />
)} )}
</> </>
) : currentView === "split" ? (
<FileEditor
toolMode={true}
multiSelect={false}
showUpload={true}
showBulkActions={true}
onFileSelect={(files) => {
setToolSelectedFiles(files);
}}
/>
) : selectedToolKey && selectedTool ? (
<ToolRenderer
selectedToolKey={selectedToolKey}
selectedTool={selectedTool}
pdfFile={activeFiles[0] || null}
files={activeFiles}
downloadUrl={downloadUrl}
setDownloadUrl={setDownloadUrl}
toolParams={getToolParams(selectedToolKey)}
updateParams={() => {}}
/>
) : ( ) : (
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<FileUploadSelector <FileUploadSelector

View File

@ -8,16 +8,17 @@ import {
Notification, Notification,
Stack, Stack,
Paper, Paper,
Text,
Alert,
Box,
Group,
} from "@mantine/core"; } from "@mantine/core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import DownloadIcon from "@mui/icons-material/Download"; import DownloadIcon from "@mui/icons-material/Download";
import { FileWithUrl } from "../types/file"; import { useFileContext } from "../contexts/FileContext";
import { fileStorage } from "../services/fileStorage"; import FileEditor from "../components/fileEditor/FileEditor";
export interface SplitPdfPanelProps { export interface SplitPdfPanelProps {
file: { file: FileWithUrl; url: string } | null;
downloadUrl?: string | null;
setDownloadUrl: (url: string | null) => void;
params: { params: {
mode: string; mode: string;
pages: string; pages: string;
@ -31,20 +32,22 @@ export interface SplitPdfPanelProps {
allowDuplicates: boolean; allowDuplicates: boolean;
}; };
updateParams: (newParams: Partial<SplitPdfPanelProps["params"]>) => void; updateParams: (newParams: Partial<SplitPdfPanelProps["params"]>) => void;
selectedFiles?: File[];
} }
const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
file,
downloadUrl,
setDownloadUrl,
params, params,
updateParams, updateParams,
selectedFiles = [],
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const fileContext = useFileContext();
const { activeFiles, selectedFileIds, updateProcessedFile } = fileContext;
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
const { const {
mode, mode,
@ -59,30 +62,30 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
allowDuplicates, allowDuplicates,
} = params; } = 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) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!file) { if (selectedFiles.length === 0) {
setStatus(t("noFileSelected")); setStatus(t("noFileSelected"));
return; return;
} }
const formData = new FormData(); const formData = new FormData();
// Handle IndexedDB files // Use selected files from context
if (!file.file.id) { selectedFiles.forEach(file => {
setStatus(t("noFileSelected")); formData.append("fileInput", file);
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);
}
let endpoint = ""; let endpoint = "";
@ -140,126 +143,232 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
} }
}; };
// 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 ( return (
<form onSubmit={handleSubmit} className="app-surface p-app-md rounded-app-md"> <Box h="100%" p="md" style={{ overflow: 'auto' }}>
<Stack gap="sm" mb={16}> <Stack gap="md">
<Select {/* Step 1: Files */}
label={t("split-by-size-or-count.type.label", "Split Mode")} {showStep1 && (
value={mode} <Paper p="md" withBorder>
onChange={(v) => v && updateParams({ mode: v })} <Text fw={500} size="lg" mb="sm">1. Files</Text>
data={[ {step1Collapsed ? (
{ value: "byPages", label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" }, <Text size="sm" c="green">
{ value: "bySections", label: t("split-by-sections.title", "Split by Grid Sections") }, Selected: {selectedFiles[0]?.name}
{ value: "bySizeOrCount", label: t("split-by-size-or-count.title", "Split by Size or Count") }, </Text>
{ value: "byChapters", label: t("splitByChapters.title", "Split by Chapters") }, ) : (
]} <Text size="sm" c="dimmed">
/> Select a PDF file in the main view to get started
</Text>
)}
</Paper>
)}
{mode === "byPages" && ( {/* Step 2: Settings */}
<TextInput {showStep2 && (
label={t("split.splitPages", "Pages")} <Paper
placeholder={t("pageSelectionPrompt", "e.g. 1,3,5-10")} p="md"
value={pages} withBorder
onChange={(e) => updateParams({ pages: e.target.value })} style={{
/> cursor: step2Collapsed ? 'pointer' : 'default',
)} opacity: step2Collapsed ? 0.8 : 1,
transition: 'opacity 0.2s ease'
}}
onClick={step2Collapsed ? () => {
// Go back to step 2
setStep2Completed(false);
} : undefined}
>
<Text fw={500} size="lg" mb="sm">2. Settings</Text>
{step2Collapsed ? (
<Text size="sm" c="green">
Settings configured <Text span c="dimmed" size="xs">(click to change)</Text>
</Text>
) : (
<Stack gap="md">
<Select
label="Choose split method"
placeholder="Select how to split the PDF"
value={mode}
onChange={(v) => 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" && ( {/* Mode-specific Parameters */}
<Stack gap="sm"> {mode === "byPages" && (
<TextInput <TextInput
label={t("split-by-sections.horizontal.label", "Horizontal Divisions")} label={t("split.splitPages", "Pages")}
type="number" placeholder={t("pageSelectionPrompt", "e.g. 1,3,5-10")}
min="0" value={pages}
max="300" onChange={(e) => updateParams({ pages: e.target.value })}
value={hDiv} />
onChange={(e) => updateParams({ hDiv: e.target.value })} )}
placeholder={t("split-by-sections.horizontal.placeholder", "Enter number of horizontal divisions")}
/>
<TextInput
label={t("split-by-sections.vertical.label", "Vertical Divisions")}
type="number"
min="0"
max="300"
value={vDiv}
onChange={(e) => updateParams({ vDiv: e.target.value })}
placeholder={t("split-by-sections.vertical.placeholder", "Enter number of vertical divisions")}
/>
<Checkbox
label={t("split-by-sections.merge", "Merge sections into one PDF")}
checked={merge}
onChange={(e) => updateParams({ merge: e.currentTarget.checked })}
/>
</Stack>
)}
{mode === "bySizeOrCount" && ( {mode === "bySections" && (
<Stack gap="sm"> <Stack gap="sm">
<Select <TextInput
label={t("split-by-size-or-count.type.label", "Split Type")} label={t("split-by-sections.horizontal.label", "Horizontal Divisions")}
value={splitType} type="number"
onChange={(v) => v && updateParams({ splitType: v })} min="0"
data={[ max="300"
{ value: "size", label: t("split-by-size-or-count.type.size", "By Size") }, value={hDiv}
{ value: "pages", label: t("split-by-size-or-count.type.pageCount", "By Page Count") }, onChange={(e) => updateParams({ hDiv: e.target.value })}
{ value: "docs", label: t("split-by-size-or-count.type.docCount", "By Document Count") }, placeholder={t("split-by-sections.horizontal.placeholder", "Enter number of horizontal divisions")}
]} />
/> <TextInput
<TextInput label={t("split-by-sections.vertical.label", "Vertical Divisions")}
label={t("split-by-size-or-count.value.label", "Split Value")} type="number"
placeholder={t("split-by-size-or-count.value.placeholder", "e.g. 10MB or 5 pages")} min="0"
value={splitValue} max="300"
onChange={(e) => updateParams({ splitValue: e.target.value })} value={vDiv}
/> onChange={(e) => updateParams({ vDiv: e.target.value })}
</Stack> placeholder={t("split-by-sections.vertical.placeholder", "Enter number of vertical divisions")}
)} />
<Checkbox
label={t("split-by-sections.merge", "Merge sections into one PDF")}
checked={merge}
onChange={(e) => updateParams({ merge: e.currentTarget.checked })}
/>
</Stack>
)}
{mode === "byChapters" && ( {mode === "bySizeOrCount" && (
<Stack gap="sm"> <Stack gap="sm">
<TextInput <Select
label={t("splitByChapters.bookmarkLevel", "Bookmark Level")} label={t("split-by-size-or-count.type.label", "Split Type")}
type="number" value={splitType}
value={bookmarkLevel} onChange={(v) => v && updateParams({ splitType: v })}
onChange={(e) => updateParams({ bookmarkLevel: e.target.value })} data={[
/> { value: "size", label: t("split-by-size-or-count.type.size", "By Size") },
<Checkbox { value: "pages", label: t("split-by-size-or-count.type.pageCount", "By Page Count") },
label={t("splitByChapters.includeMetadata", "Include Metadata")} { value: "docs", label: t("split-by-size-or-count.type.docCount", "By Document Count") },
checked={includeMetadata} ]}
onChange={(e) => updateParams({ includeMetadata: e.currentTarget.checked })} />
/> <TextInput
<Checkbox label={t("split-by-size-or-count.value.label", "Split Value")}
label={t("splitByChapters.allowDuplicates", "Allow Duplicate Bookmarks")} placeholder={t("split-by-size-or-count.value.placeholder", "e.g. 10MB or 5 pages")}
checked={allowDuplicates} value={splitValue}
onChange={(e) => updateParams({ allowDuplicates: e.currentTarget.checked })} onChange={(e) => updateParams({ splitValue: e.target.value })}
/> />
</Stack> </Stack>
)} )}
<Button type="submit" loading={isLoading} fullWidth> {mode === "byChapters" && (
{isLoading ? t("loading") : t("split.submit", "Split PDF")} <Stack gap="sm">
</Button> <TextInput
label={t("splitByChapters.bookmarkLevel", "Bookmark Level")}
type="number"
value={bookmarkLevel}
onChange={(e) => updateParams({ bookmarkLevel: e.target.value })}
/>
<Checkbox
label={t("splitByChapters.includeMetadata", "Include Metadata")}
checked={includeMetadata}
onChange={(e) => updateParams({ includeMetadata: e.currentTarget.checked })}
/>
<Checkbox
label={t("splitByChapters.allowDuplicates", "Allow Duplicate Bookmarks")}
checked={allowDuplicates}
onChange={(e) => updateParams({ allowDuplicates: e.currentTarget.checked })}
/>
</Stack>
)}
{status && <p className="text-xs text-text-muted">{status}</p>} {/* Done Button */}
{mode && (
<Button
fullWidth
mt="md"
disabled={!step2SettingsValid}
onClick={() => setStep2Completed(true)}
>
Done
</Button>
)}
</Stack>
)}
</Paper>
)}
{errorMessage && ( {/* Step 3: Review */}
<Notification color="red" title={t("error._value", "Error")} onClose={() => setErrorMessage(null)}> {showStep3 && (
{errorMessage} <Paper p="md" withBorder>
</Notification> <Text fw={500} size="lg" mb="sm">3. Review</Text>
)}
<form onSubmit={handleSubmit}>
<Button
type="submit"
loading={isLoading}
fullWidth
disabled={selectedFiles.length === 0}
>
{isLoading ? t("loading") : t("split.submit", "Split PDF")}
</Button>
{status === t("downloadComplete") && downloadUrl && ( {status && <Text size="xs" c="dimmed" mt="xs">{status}</Text>}
<Button
component="a" {errorMessage && (
href={downloadUrl} <Notification color="red" title={t("error._value", "Error")} onClose={() => setErrorMessage(null)} mt="sm">
download="split_output.zip" {errorMessage}
leftSection={<DownloadIcon />} </Notification>
color="green" )}
fullWidth
> {downloadUrl && (
{t("downloadPdf", "Download Split PDF")} <Button
</Button> component="a"
)} href={downloadUrl}
</Stack> download="split_output.zip"
</form> leftSection={<DownloadIcon />}
color="green"
fullWidth
mt="sm"
>
{t("downloadPdf", "Download Split PDF")}
</Button>
)}
</form>
</Paper>
)}
</Stack>
</Box>
); );
}; };