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 { 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) => (
|
||||||
|
@ -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
|
||||||
|
@ -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":
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user