mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Split reimplementation
This commit is contained in:
parent
666c15fabd
commit
c7dce8a68f
@ -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<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [localLoading, setLocalLoading] = useState(false);
|
||||
const [csvInput, setCsvInput] = useState<string>('');
|
||||
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<string | null>(null);
|
||||
const [dropTarget, setDropTarget] = useState<string | null>(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 = ({
|
||||
|
||||
<Box p="md" pt="xl">
|
||||
<Group mb="md">
|
||||
<Button
|
||||
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 && (
|
||||
{showBulkActions && !toolMode && (
|
||||
<>
|
||||
<Button onClick={selectAll} variant="light">Select All</Button>
|
||||
<Button onClick={deselectAll} variant="light">Deselect All</Button>
|
||||
@ -424,35 +423,31 @@ const FileEditor = ({
|
||||
)}
|
||||
|
||||
{/* Load from storage and upload buttons */}
|
||||
<Button
|
||||
variant="outline"
|
||||
color="blue"
|
||||
onClick={() => setShowFilePickerModal(true)}
|
||||
>
|
||||
Load from Storage
|
||||
</Button>
|
||||
{showUpload && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
color="blue"
|
||||
onClick={() => setShowFilePickerModal(true)}
|
||||
>
|
||||
Load from Storage
|
||||
</Button>
|
||||
|
||||
<Dropzone
|
||||
onDrop={handleFileUpload}
|
||||
accept={["application/pdf"]}
|
||||
multiple={true}
|
||||
maxSize={2 * 1024 * 1024 * 1024}
|
||||
style={{ display: 'contents' }}
|
||||
>
|
||||
<Button variant="outline" color="green">
|
||||
Upload Files
|
||||
</Button>
|
||||
</Dropzone>
|
||||
<Dropzone
|
||||
onDrop={handleFileUpload}
|
||||
accept={["application/pdf"]}
|
||||
multiple={true}
|
||||
maxSize={2 * 1024 * 1024 * 1024}
|
||||
style={{ display: 'contents' }}
|
||||
>
|
||||
<Button variant="outline" color="green">
|
||||
Upload Files
|
||||
</Button>
|
||||
</Dropzone>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{selectionMode && (
|
||||
<BulkSelectionPanel
|
||||
csvInput={csvInput}
|
||||
setCsvInput={setCsvInput}
|
||||
selectedPages={localSelectedFiles}
|
||||
onUpdatePagesFromCSV={updateFilesFromCSV}
|
||||
/>
|
||||
)}
|
||||
|
||||
{files.length === 0 && !localLoading ? (
|
||||
<Center h="60vh">
|
||||
@ -530,6 +525,7 @@ const FileEditor = ({
|
||||
onMergeFromHere={handleMergeFromHere}
|
||||
onSplitFile={handleSplitFile}
|
||||
onSetStatus={setStatus}
|
||||
toolMode={toolMode}
|
||||
/>
|
||||
)}
|
||||
renderSplitMarker={(file, index) => (
|
||||
|
@ -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'
|
||||
}}
|
||||
>
|
||||
<Tooltip label="View File">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewFile(file.id);
|
||||
onSetStatus(`Opened ${file.name}`);
|
||||
}}
|
||||
>
|
||||
<VisibilityIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{!toolMode && (
|
||||
<>
|
||||
<Tooltip label="View File">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onViewFile(file.id);
|
||||
onSetStatus(`Opened ${file.name}`);
|
||||
}}
|
||||
>
|
||||
<VisibilityIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Merge from here">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMergeFromHere(file.id);
|
||||
onSetStatus(`Starting merge from ${file.name}`);
|
||||
}}
|
||||
>
|
||||
<MergeIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Merge from here">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMergeFromHere(file.id);
|
||||
onSetStatus(`Starting merge from ${file.name}`);
|
||||
}}
|
||||
>
|
||||
<MergeIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Split File">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSplitFile(file.id);
|
||||
onSetStatus(`Opening ${file.name} in page editor`);
|
||||
}}
|
||||
>
|
||||
<SplitscreenIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Split File">
|
||||
<ActionIcon
|
||||
size="md"
|
||||
variant="subtle"
|
||||
c="white"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSplitFile(file.id);
|
||||
onSetStatus(`Opening ${file.name} in page editor`);
|
||||
}}
|
||||
>
|
||||
<SplitscreenIcon style={{ fontSize: 20 }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label="Delete File">
|
||||
<ActionIcon
|
||||
|
@ -10,6 +10,7 @@ interface ToolRendererProps {
|
||||
setDownloadUrl: (url: string | null) => 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 <div>Tool not found</div>;
|
||||
@ -33,11 +35,9 @@ const ToolRenderer = ({
|
||||
case "split":
|
||||
return (
|
||||
<ToolComponent
|
||||
file={pdfFile}
|
||||
downloadUrl={downloadUrl}
|
||||
setDownloadUrl={setDownloadUrl}
|
||||
params={toolParams}
|
||||
updateParams={updateParams}
|
||||
selectedFiles={toolSelectedFiles}
|
||||
/>
|
||||
);
|
||||
case "compress":
|
||||
|
@ -36,7 +36,7 @@ type ToolRegistry = {
|
||||
|
||||
// Base tool registry without translations
|
||||
const baseToolRegistry = {
|
||||
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "viewer" },
|
||||
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "split" },
|
||||
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "viewer" },
|
||||
merge: { icon: <AddToPhotosIcon />, 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<any>(null);
|
||||
const [toolSelectedFiles, setToolSelectedFiles] = useState<File[]>([]);
|
||||
const [toolParams, setToolParams] = useState<Record<string, any>>({});
|
||||
|
||||
// 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}
|
||||
/>
|
||||
</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' }}>
|
||||
<FileUploadSelector
|
||||
|
@ -8,16 +8,17 @@ import {
|
||||
Notification,
|
||||
Stack,
|
||||
Paper,
|
||||
Text,
|
||||
Alert,
|
||||
Box,
|
||||
Group,
|
||||
} from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { FileWithUrl } from "../types/file";
|
||||
import { fileStorage } from "../services/fileStorage";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import FileEditor from "../components/fileEditor/FileEditor";
|
||||
|
||||
export interface SplitPdfPanelProps {
|
||||
file: { file: FileWithUrl; url: string } | null;
|
||||
downloadUrl?: string | null;
|
||||
setDownloadUrl: (url: string | null) => void;
|
||||
params: {
|
||||
mode: string;
|
||||
pages: string;
|
||||
@ -31,20 +32,22 @@ export interface SplitPdfPanelProps {
|
||||
allowDuplicates: boolean;
|
||||
};
|
||||
updateParams: (newParams: Partial<SplitPdfPanelProps["params"]>) => void;
|
||||
selectedFiles?: File[];
|
||||
}
|
||||
|
||||
const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
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<string | null>(null);
|
||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
mode,
|
||||
@ -59,30 +62,30 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
||||
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<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 (
|
||||
<form onSubmit={handleSubmit} className="app-surface p-app-md rounded-app-md">
|
||||
<Stack gap="sm" mb={16}>
|
||||
<Select
|
||||
label={t("split-by-size-or-count.type.label", "Split Mode")}
|
||||
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") },
|
||||
]}
|
||||
/>
|
||||
<Box h="100%" p="md" style={{ overflow: 'auto' }}>
|
||||
<Stack gap="md">
|
||||
{/* Step 1: Files */}
|
||||
{showStep1 && (
|
||||
<Paper p="md" withBorder>
|
||||
<Text fw={500} size="lg" mb="sm">1. Files</Text>
|
||||
{step1Collapsed ? (
|
||||
<Text size="sm" c="green">
|
||||
✓ Selected: {selectedFiles[0]?.name}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
Select a PDF file in the main view to get started
|
||||
</Text>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{mode === "byPages" && (
|
||||
<TextInput
|
||||
label={t("split.splitPages", "Pages")}
|
||||
placeholder={t("pageSelectionPrompt", "e.g. 1,3,5-10")}
|
||||
value={pages}
|
||||
onChange={(e) => updateParams({ pages: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{/* Step 2: Settings */}
|
||||
{showStep2 && (
|
||||
<Paper
|
||||
p="md"
|
||||
withBorder
|
||||
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" && (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label={t("split-by-sections.horizontal.label", "Horizontal Divisions")}
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
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-specific Parameters */}
|
||||
{mode === "byPages" && (
|
||||
<TextInput
|
||||
label={t("split.splitPages", "Pages")}
|
||||
placeholder={t("pageSelectionPrompt", "e.g. 1,3,5-10")}
|
||||
value={pages}
|
||||
onChange={(e) => updateParams({ pages: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === "bySizeOrCount" && (
|
||||
<Stack gap="sm">
|
||||
<Select
|
||||
label={t("split-by-size-or-count.type.label", "Split Type")}
|
||||
value={splitType}
|
||||
onChange={(v) => 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") },
|
||||
]}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("split-by-size-or-count.value.label", "Split Value")}
|
||||
placeholder={t("split-by-size-or-count.value.placeholder", "e.g. 10MB or 5 pages")}
|
||||
value={splitValue}
|
||||
onChange={(e) => updateParams({ splitValue: e.target.value })}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{mode === "bySections" && (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label={t("split-by-sections.horizontal.label", "Horizontal Divisions")}
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
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 === "byChapters" && (
|
||||
<Stack gap="sm">
|
||||
<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>
|
||||
)}
|
||||
{mode === "bySizeOrCount" && (
|
||||
<Stack gap="sm">
|
||||
<Select
|
||||
label={t("split-by-size-or-count.type.label", "Split Type")}
|
||||
value={splitType}
|
||||
onChange={(v) => 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") },
|
||||
]}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("split-by-size-or-count.value.label", "Split Value")}
|
||||
placeholder={t("split-by-size-or-count.value.placeholder", "e.g. 10MB or 5 pages")}
|
||||
value={splitValue}
|
||||
onChange={(e) => updateParams({ splitValue: e.target.value })}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Button type="submit" loading={isLoading} fullWidth>
|
||||
{isLoading ? t("loading") : t("split.submit", "Split PDF")}
|
||||
</Button>
|
||||
{mode === "byChapters" && (
|
||||
<Stack gap="sm">
|
||||
<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 && (
|
||||
<Notification color="red" title={t("error._value", "Error")} onClose={() => setErrorMessage(null)}>
|
||||
{errorMessage}
|
||||
</Notification>
|
||||
)}
|
||||
{/* Step 3: Review */}
|
||||
{showStep3 && (
|
||||
<Paper p="md" withBorder>
|
||||
<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 && (
|
||||
<Button
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
download="split_output.zip"
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
>
|
||||
{t("downloadPdf", "Download Split PDF")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
{status && <Text size="xs" c="dimmed" mt="xs">{status}</Text>}
|
||||
|
||||
{errorMessage && (
|
||||
<Notification color="red" title={t("error._value", "Error")} onClose={() => setErrorMessage(null)} mt="sm">
|
||||
{errorMessage}
|
||||
</Notification>
|
||||
)}
|
||||
|
||||
{downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
download="split_output.zip"
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
>
|
||||
{t("downloadPdf", "Download Split PDF")}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user