mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Split refactor
This commit is contained in:
parent
37484f64f9
commit
c100c9f10d
35
frontend/src/components/tools/shared/ErrorNotification.tsx
Normal file
35
frontend/src/components/tools/shared/ErrorNotification.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Notification } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export interface ErrorNotificationProps {
|
||||||
|
error: string | null;
|
||||||
|
onClose: () => void;
|
||||||
|
title?: string;
|
||||||
|
color?: string;
|
||||||
|
mb?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorNotification = ({
|
||||||
|
error,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
color = 'red',
|
||||||
|
mb = 'md'
|
||||||
|
}: ErrorNotificationProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!error) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Notification
|
||||||
|
color={color}
|
||||||
|
title={title || t("error._value", "Error")}
|
||||||
|
onClose={onClose}
|
||||||
|
mb={mb}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</Notification>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorNotification;
|
40
frontend/src/components/tools/shared/FileStatusIndicator.tsx
Normal file
40
frontend/src/components/tools/shared/FileStatusIndicator.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Text } from '@mantine/core';
|
||||||
|
|
||||||
|
export interface FileStatusIndicatorProps {
|
||||||
|
selectedFiles?: File[];
|
||||||
|
isCompleted?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
showFileName?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileStatusIndicator = ({
|
||||||
|
selectedFiles = [],
|
||||||
|
isCompleted = false,
|
||||||
|
placeholder = "Select a PDF file in the main view to get started",
|
||||||
|
showFileName = true
|
||||||
|
}: FileStatusIndicatorProps) => {
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{placeholder}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCompleted) {
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="green">
|
||||||
|
✓ Selected: {showFileName ? selectedFiles[0]?.name : `${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''}`}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="blue">
|
||||||
|
Selected: {showFileName ? selectedFiles[0]?.name : `${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''}`}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileStatusIndicator;
|
51
frontend/src/components/tools/shared/OperationButton.tsx
Normal file
51
frontend/src/components/tools/shared/OperationButton.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export interface OperationButtonProps {
|
||||||
|
onClick?: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
loadingText?: string;
|
||||||
|
submitText?: string;
|
||||||
|
variant?: 'filled' | 'outline' | 'subtle';
|
||||||
|
color?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
mt?: string;
|
||||||
|
type?: 'button' | 'submit' | 'reset';
|
||||||
|
}
|
||||||
|
|
||||||
|
const OperationButton = ({
|
||||||
|
onClick,
|
||||||
|
isLoading = false,
|
||||||
|
disabled = false,
|
||||||
|
loadingText,
|
||||||
|
submitText,
|
||||||
|
variant = 'filled',
|
||||||
|
color = 'blue',
|
||||||
|
fullWidth = true,
|
||||||
|
mt = 'md',
|
||||||
|
type = 'button'
|
||||||
|
}: OperationButtonProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={type}
|
||||||
|
onClick={onClick}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
mt={mt}
|
||||||
|
loading={isLoading}
|
||||||
|
disabled={disabled}
|
||||||
|
variant={variant}
|
||||||
|
color={color}
|
||||||
|
>
|
||||||
|
{isLoading
|
||||||
|
? (loadingText || t("loading", "Loading..."))
|
||||||
|
: (submitText || t("submit", "Submit"))
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OperationButton;
|
112
frontend/src/components/tools/shared/ResultsPreview.tsx
Normal file
112
frontend/src/components/tools/shared/ResultsPreview.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import { Grid, Paper, Box, Image, Text, Loader, Stack, Center } from '@mantine/core';
|
||||||
|
|
||||||
|
export interface ResultFile {
|
||||||
|
file: File;
|
||||||
|
thumbnail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResultsPreviewProps {
|
||||||
|
files: ResultFile[];
|
||||||
|
isGeneratingThumbnails?: boolean;
|
||||||
|
onFileClick?: (file: File) => void;
|
||||||
|
title?: string;
|
||||||
|
emptyMessage?: string;
|
||||||
|
loadingMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResultsPreview = ({
|
||||||
|
files,
|
||||||
|
isGeneratingThumbnails = false,
|
||||||
|
onFileClick,
|
||||||
|
title,
|
||||||
|
emptyMessage = "No files to preview",
|
||||||
|
loadingMessage = "Generating previews..."
|
||||||
|
}: ResultsPreviewProps) => {
|
||||||
|
const formatSize = (size: number) => {
|
||||||
|
if (size > 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
if (size > 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||||
|
return `${size} B`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (files.length === 0 && !isGeneratingThumbnails) {
|
||||||
|
return (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{emptyMessage}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mt="lg" p="md" style={{ backgroundColor: 'var(--mantine-color-gray-0)', borderRadius: 8 }}>
|
||||||
|
{title && (
|
||||||
|
<Text fw={500} size="md" mb="sm">
|
||||||
|
{title} ({files.length} files)
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isGeneratingThumbnails ? (
|
||||||
|
<Center p="lg">
|
||||||
|
<Stack align="center" gap="sm">
|
||||||
|
<Loader size="sm" />
|
||||||
|
<Text size="sm" c="dimmed">{loadingMessage}</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Grid>
|
||||||
|
{files.map((result, index) => (
|
||||||
|
<Grid.Col span={{ base: 6, sm: 4, md: 3 }} key={index}>
|
||||||
|
<Paper
|
||||||
|
p="xs"
|
||||||
|
withBorder
|
||||||
|
onClick={() => onFileClick?.(result.file)}
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
height: '10rem',
|
||||||
|
width:'5rem',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
cursor: onFileClick ? 'pointer' : 'default',
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
{result.thumbnail ? (
|
||||||
|
<Image
|
||||||
|
src={result.thumbnail}
|
||||||
|
alt={`Preview of ${result.file.name}`}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '9rem',
|
||||||
|
objectFit: 'contain'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text size="xs" c="dimmed">No preview</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="dimmed"
|
||||||
|
mt="xs"
|
||||||
|
style={{
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
title={result.file.name}
|
||||||
|
>
|
||||||
|
{result.file.name}
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{formatSize(result.file.size)}
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResultsPreview;
|
120
frontend/src/components/tools/shared/ToolStep.tsx
Normal file
120
frontend/src/components/tools/shared/ToolStep.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import React, { createContext, useContext, useMemo, useRef } from 'react';
|
||||||
|
import { Paper, Text, Stack, Box } from '@mantine/core';
|
||||||
|
|
||||||
|
interface ToolStepContextType {
|
||||||
|
visibleStepCount: number;
|
||||||
|
getStepNumber: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolStepContext = createContext<ToolStepContextType | null>(null);
|
||||||
|
|
||||||
|
export interface ToolStepProps {
|
||||||
|
title: string;
|
||||||
|
isVisible?: boolean;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
isCompleted?: boolean;
|
||||||
|
onCollapsedClick?: () => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
completedMessage?: string;
|
||||||
|
helpText?: string;
|
||||||
|
showNumber?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolStep = ({
|
||||||
|
title,
|
||||||
|
isVisible = true,
|
||||||
|
isCollapsed = false,
|
||||||
|
isCompleted = false,
|
||||||
|
onCollapsedClick,
|
||||||
|
children,
|
||||||
|
completedMessage,
|
||||||
|
helpText,
|
||||||
|
showNumber
|
||||||
|
}: ToolStepProps) => {
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
// Auto-detect if we should show numbers based on sibling count
|
||||||
|
const shouldShowNumber = useMemo(() => {
|
||||||
|
if (showNumber !== undefined) return showNumber;
|
||||||
|
const parent = useContext(ToolStepContext);
|
||||||
|
return parent ? parent.visibleStepCount >= 3 : false;
|
||||||
|
}, [showNumber]);
|
||||||
|
|
||||||
|
const stepNumber = useContext(ToolStepContext)?.getStepNumber?.() || 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
p="md"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
cursor: isCollapsed && onCollapsedClick ? 'pointer' : 'default',
|
||||||
|
opacity: isCollapsed ? 0.8 : 1,
|
||||||
|
transition: 'opacity 0.2s ease'
|
||||||
|
}}
|
||||||
|
onClick={isCollapsed && onCollapsedClick ? onCollapsedClick : undefined}
|
||||||
|
>
|
||||||
|
<Text fw={500} size="lg" mb="sm">
|
||||||
|
{shouldShowNumber ? `${stepNumber}. ` : ''}{title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{isCollapsed ? (
|
||||||
|
<Box>
|
||||||
|
{isCompleted && completedMessage && (
|
||||||
|
<Text size="sm" c="green">
|
||||||
|
✓ {completedMessage}
|
||||||
|
{onCollapsedClick && (
|
||||||
|
<Text span c="dimmed" size="xs" ml="sm">
|
||||||
|
(click to change)
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Stack gap="md">
|
||||||
|
{helpText && (
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{helpText}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolStepContainerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolStepContainer = ({ children }: ToolStepContainerProps) => {
|
||||||
|
const stepCounterRef = useRef(0);
|
||||||
|
|
||||||
|
// Count visible ToolStep children
|
||||||
|
const visibleStepCount = useMemo(() => {
|
||||||
|
let count = 0;
|
||||||
|
React.Children.forEach(children, (child) => {
|
||||||
|
if (React.isValidElement(child) && child.type === ToolStep) {
|
||||||
|
const isVisible = child.props.isVisible !== false;
|
||||||
|
if (isVisible) count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return count;
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
const contextValue = useMemo(() => ({
|
||||||
|
visibleStepCount,
|
||||||
|
getStepNumber: () => ++stepCounterRef.current
|
||||||
|
}), [visibleStepCount]);
|
||||||
|
|
||||||
|
stepCounterRef.current = 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolStepContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</ToolStepContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToolStep;
|
148
frontend/src/components/tools/split/SplitSettings.tsx
Normal file
148
frontend/src/components/tools/split/SplitSettings.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { Stack, TextInput, Select, Checkbox } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants';
|
||||||
|
|
||||||
|
export interface SplitParameters {
|
||||||
|
pages: string;
|
||||||
|
hDiv: string;
|
||||||
|
vDiv: string;
|
||||||
|
merge: boolean;
|
||||||
|
splitType: SplitType | '';
|
||||||
|
splitValue: string;
|
||||||
|
bookmarkLevel: string;
|
||||||
|
includeMetadata: boolean;
|
||||||
|
allowDuplicates: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SplitSettingsProps {
|
||||||
|
mode: SplitMode | '';
|
||||||
|
onModeChange: (mode: SplitMode | '') => void;
|
||||||
|
parameters: SplitParameters;
|
||||||
|
onParameterChange: (parameter: keyof SplitParameters, value: string | boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SplitSettings = ({
|
||||||
|
mode,
|
||||||
|
onModeChange,
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}: SplitSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const renderByPagesForm = () => (
|
||||||
|
<TextInput
|
||||||
|
label={t("split.splitPages", "Pages")}
|
||||||
|
placeholder={t("pageSelectionPrompt", "e.g. 1,3,5-10")}
|
||||||
|
value={parameters.pages}
|
||||||
|
onChange={(e) => onParameterChange('pages', e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderBySectionsForm = () => (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<TextInput
|
||||||
|
label={t("split-by-sections.horizontal.label", "Horizontal Divisions")}
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="300"
|
||||||
|
value={parameters.hDiv}
|
||||||
|
onChange={(e) => onParameterChange('hDiv', e.target.value)}
|
||||||
|
placeholder={t("split-by-sections.horizontal.placeholder", "Enter number of horizontal divisions")}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t("split-by-sections.vertical.label", "Vertical Divisions")}
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="300"
|
||||||
|
value={parameters.vDiv}
|
||||||
|
onChange={(e) => onParameterChange('vDiv', e.target.value)}
|
||||||
|
placeholder={t("split-by-sections.vertical.placeholder", "Enter number of vertical divisions")}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t("split-by-sections.merge", "Merge sections into one PDF")}
|
||||||
|
checked={parameters.merge}
|
||||||
|
onChange={(e) => onParameterChange('merge', e.currentTarget.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderBySizeOrCountForm = () => (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Select
|
||||||
|
label={t("split-by-size-or-count.type.label", "Split Type")}
|
||||||
|
value={parameters.splitType}
|
||||||
|
onChange={(v) => v && onParameterChange('splitType', v)}
|
||||||
|
disabled={disabled}
|
||||||
|
data={[
|
||||||
|
{ value: SPLIT_TYPES.SIZE, label: t("split-by-size-or-count.type.size", "By Size") },
|
||||||
|
{ value: SPLIT_TYPES.PAGES, label: t("split-by-size-or-count.type.pageCount", "By Page Count") },
|
||||||
|
{ value: SPLIT_TYPES.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={parameters.splitValue}
|
||||||
|
onChange={(e) => onParameterChange('splitValue', e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderByChaptersForm = () => (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<TextInput
|
||||||
|
label={t("splitByChapters.bookmarkLevel", "Bookmark Level")}
|
||||||
|
type="number"
|
||||||
|
value={parameters.bookmarkLevel}
|
||||||
|
onChange={(e) => onParameterChange('bookmarkLevel', e.target.value)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t("splitByChapters.includeMetadata", "Include Metadata")}
|
||||||
|
checked={parameters.includeMetadata}
|
||||||
|
onChange={(e) => onParameterChange('includeMetadata', e.currentTarget.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={t("splitByChapters.allowDuplicates", "Allow Duplicate Bookmarks")}
|
||||||
|
checked={parameters.allowDuplicates}
|
||||||
|
onChange={(e) => onParameterChange('allowDuplicates', e.currentTarget.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
{/* Mode Selector */}
|
||||||
|
<Select
|
||||||
|
label="Choose split method"
|
||||||
|
placeholder="Select how to split the PDF"
|
||||||
|
value={mode}
|
||||||
|
onChange={(v) => v && onModeChange(v)}
|
||||||
|
disabled={disabled}
|
||||||
|
data={[
|
||||||
|
{ value: SPLIT_MODES.BY_PAGES, label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" },
|
||||||
|
{ value: SPLIT_MODES.BY_SECTIONS, label: t("split-by-sections.title", "Split by Grid Sections") },
|
||||||
|
{ value: SPLIT_MODES.BY_SIZE_OR_COUNT, label: t("split-by-size-or-count.title", "Split by Size or Count") },
|
||||||
|
{ value: SPLIT_MODES.BY_CHAPTERS, label: t("splitByChapters.title", "Split by Chapters") },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Parameter Form */}
|
||||||
|
{mode === SPLIT_MODES.BY_PAGES && renderByPagesForm()}
|
||||||
|
{mode === SPLIT_MODES.BY_SECTIONS && renderBySectionsForm()}
|
||||||
|
{mode === SPLIT_MODES.BY_SIZE_OR_COUNT && renderBySizeOrCountForm()}
|
||||||
|
{mode === SPLIT_MODES.BY_CHAPTERS && renderByChaptersForm()}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SplitSettings;
|
22
frontend/src/constants/splitConstants.ts
Normal file
22
frontend/src/constants/splitConstants.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export const SPLIT_MODES = {
|
||||||
|
BY_PAGES: 'byPages',
|
||||||
|
BY_SECTIONS: 'bySections',
|
||||||
|
BY_SIZE_OR_COUNT: 'bySizeOrCount',
|
||||||
|
BY_CHAPTERS: 'byChapters'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const SPLIT_TYPES = {
|
||||||
|
SIZE: 'size',
|
||||||
|
PAGES: 'pages',
|
||||||
|
DOCS: 'docs'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ENDPOINTS = {
|
||||||
|
[SPLIT_MODES.BY_PAGES]: 'split-pages',
|
||||||
|
[SPLIT_MODES.BY_SECTIONS]: 'split-pdf-by-sections',
|
||||||
|
[SPLIT_MODES.BY_SIZE_OR_COUNT]: 'split-by-size-or-count',
|
||||||
|
[SPLIT_MODES.BY_CHAPTERS]: 'split-pdf-by-chapters'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type SplitMode = typeof SPLIT_MODES[keyof typeof SPLIT_MODES];
|
||||||
|
export type SplitType = typeof SPLIT_TYPES[keyof typeof SPLIT_TYPES];
|
67
frontend/src/hooks/tools/shared/useOperationResults.ts
Normal file
67
frontend/src/hooks/tools/shared/useOperationResults.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
export interface OperationResult {
|
||||||
|
files: File[];
|
||||||
|
thumbnails: string[];
|
||||||
|
isGeneratingThumbnails: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationResultsHook {
|
||||||
|
results: OperationResult;
|
||||||
|
downloadUrl: string | null;
|
||||||
|
status: string;
|
||||||
|
errorMessage: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
|
||||||
|
setResults: (results: OperationResult) => void;
|
||||||
|
setDownloadUrl: (url: string | null) => void;
|
||||||
|
setStatus: (status: string) => void;
|
||||||
|
setErrorMessage: (error: string | null) => void;
|
||||||
|
setIsLoading: (loading: boolean) => void;
|
||||||
|
|
||||||
|
resetResults: () => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialResults: OperationResult = {
|
||||||
|
files: [],
|
||||||
|
thumbnails: [],
|
||||||
|
isGeneratingThumbnails: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOperationResults = (): OperationResultsHook => {
|
||||||
|
const [results, setResults] = useState<OperationResult>(initialResults);
|
||||||
|
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const resetResults = useCallback(() => {
|
||||||
|
setResults(initialResults);
|
||||||
|
setDownloadUrl(null);
|
||||||
|
setStatus('');
|
||||||
|
setErrorMessage(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
results,
|
||||||
|
downloadUrl,
|
||||||
|
status,
|
||||||
|
errorMessage,
|
||||||
|
isLoading,
|
||||||
|
|
||||||
|
setResults,
|
||||||
|
setDownloadUrl,
|
||||||
|
setStatus,
|
||||||
|
setErrorMessage,
|
||||||
|
setIsLoading,
|
||||||
|
|
||||||
|
resetResults,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
};
|
242
frontend/src/hooks/tools/split/useSplitOperation.ts
Normal file
242
frontend/src/hooks/tools/split/useSplitOperation.ts
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useFileContext } from '../../../contexts/FileContext';
|
||||||
|
import { FileOperation } from '../../../types/fileContext';
|
||||||
|
import { zipFileService } from '../../../services/zipFileService';
|
||||||
|
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||||
|
import { SplitParameters } from '../../../components/tools/split/SplitSettings';
|
||||||
|
import { SPLIT_MODES, ENDPOINTS, type SplitMode } from '../../../constants/splitConstants';
|
||||||
|
|
||||||
|
export interface SplitOperationHook {
|
||||||
|
executeOperation: (
|
||||||
|
mode: SplitMode | '',
|
||||||
|
parameters: SplitParameters,
|
||||||
|
selectedFiles: File[]
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
// Flattened result properties for cleaner access
|
||||||
|
files: File[];
|
||||||
|
thumbnails: string[];
|
||||||
|
isGeneratingThumbnails: boolean;
|
||||||
|
downloadUrl: string | null;
|
||||||
|
status: string;
|
||||||
|
errorMessage: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
|
||||||
|
// Result management functions
|
||||||
|
resetResults: () => void;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSplitOperation = (): SplitOperationHook => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
recordOperation,
|
||||||
|
markOperationApplied,
|
||||||
|
markOperationFailed,
|
||||||
|
addFiles
|
||||||
|
} = useFileContext();
|
||||||
|
|
||||||
|
// Internal state management (replacing useOperationResults)
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [thumbnails, setThumbnails] = useState<string[]>([]);
|
||||||
|
const [isGeneratingThumbnails, setIsGeneratingThumbnails] = useState(false);
|
||||||
|
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||||
|
const [status, setStatus] = useState('');
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const buildFormData = useCallback((
|
||||||
|
mode: SplitMode | '',
|
||||||
|
parameters: SplitParameters,
|
||||||
|
selectedFiles: File[]
|
||||||
|
) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
selectedFiles.forEach(file => {
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!mode) {
|
||||||
|
throw new Error('Split mode is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint = "";
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case SPLIT_MODES.BY_PAGES:
|
||||||
|
formData.append("pageNumbers", parameters.pages);
|
||||||
|
endpoint = "/api/v1/general/split-pages";
|
||||||
|
break;
|
||||||
|
case SPLIT_MODES.BY_SECTIONS:
|
||||||
|
formData.append("horizontalDivisions", parameters.hDiv);
|
||||||
|
formData.append("verticalDivisions", parameters.vDiv);
|
||||||
|
formData.append("merge", parameters.merge.toString());
|
||||||
|
endpoint = "/api/v1/general/split-pdf-by-sections";
|
||||||
|
break;
|
||||||
|
case SPLIT_MODES.BY_SIZE_OR_COUNT:
|
||||||
|
formData.append(
|
||||||
|
"splitType",
|
||||||
|
parameters.splitType === "size" ? "0" : parameters.splitType === "pages" ? "1" : "2"
|
||||||
|
);
|
||||||
|
formData.append("splitValue", parameters.splitValue);
|
||||||
|
endpoint = "/api/v1/general/split-by-size-or-count";
|
||||||
|
break;
|
||||||
|
case SPLIT_MODES.BY_CHAPTERS:
|
||||||
|
formData.append("bookmarkLevel", parameters.bookmarkLevel);
|
||||||
|
formData.append("includeMetadata", parameters.includeMetadata.toString());
|
||||||
|
formData.append("allowDuplicates", parameters.allowDuplicates.toString());
|
||||||
|
endpoint = "/api/v1/general/split-pdf-by-chapters";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown split mode: ${mode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { formData, endpoint };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const createOperation = useCallback((
|
||||||
|
mode: SplitMode | '',
|
||||||
|
parameters: SplitParameters,
|
||||||
|
selectedFiles: File[]
|
||||||
|
): { operation: FileOperation; operationId: string; fileId: string } => {
|
||||||
|
const operationId = `split-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const fileId = selectedFiles[0].name;
|
||||||
|
|
||||||
|
const operation: FileOperation = {
|
||||||
|
id: operationId,
|
||||||
|
type: 'split',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
fileIds: selectedFiles.map(f => f.name),
|
||||||
|
status: 'pending',
|
||||||
|
metadata: {
|
||||||
|
originalFileName: selectedFiles[0].name,
|
||||||
|
parameters: {
|
||||||
|
mode,
|
||||||
|
pages: mode === SPLIT_MODES.BY_PAGES ? parameters.pages : undefined,
|
||||||
|
hDiv: mode === SPLIT_MODES.BY_SECTIONS ? parameters.hDiv : undefined,
|
||||||
|
vDiv: mode === SPLIT_MODES.BY_SECTIONS ? parameters.vDiv : undefined,
|
||||||
|
merge: mode === SPLIT_MODES.BY_SECTIONS ? parameters.merge : undefined,
|
||||||
|
splitType: mode === SPLIT_MODES.BY_SIZE_OR_COUNT ? parameters.splitType : undefined,
|
||||||
|
splitValue: mode === SPLIT_MODES.BY_SIZE_OR_COUNT ? parameters.splitValue : undefined,
|
||||||
|
bookmarkLevel: mode === SPLIT_MODES.BY_CHAPTERS ? parameters.bookmarkLevel : undefined,
|
||||||
|
includeMetadata: mode === SPLIT_MODES.BY_CHAPTERS ? parameters.includeMetadata : undefined,
|
||||||
|
allowDuplicates: mode === SPLIT_MODES.BY_CHAPTERS ? parameters.allowDuplicates : undefined,
|
||||||
|
},
|
||||||
|
fileSize: selectedFiles[0].size
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { operation, operationId, fileId };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const processResults = useCallback(async (blob: Blob) => {
|
||||||
|
try {
|
||||||
|
const zipFile = new File([blob], "split_result.zip", { type: "application/zip" });
|
||||||
|
const extractionResult = await zipFileService.extractPdfFiles(zipFile);
|
||||||
|
|
||||||
|
if (extractionResult.success && extractionResult.extractedFiles.length > 0) {
|
||||||
|
// Set local state for preview
|
||||||
|
setFiles(extractionResult.extractedFiles);
|
||||||
|
setThumbnails([]);
|
||||||
|
setIsGeneratingThumbnails(true);
|
||||||
|
|
||||||
|
// Add extracted files to FileContext for future use
|
||||||
|
await addFiles(extractionResult.extractedFiles);
|
||||||
|
|
||||||
|
const thumbnails = await Promise.all(
|
||||||
|
extractionResult.extractedFiles.map(async (file) => {
|
||||||
|
try {
|
||||||
|
return await generateThumbnailForFile(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setThumbnails(thumbnails);
|
||||||
|
setIsGeneratingThumbnails(false);
|
||||||
|
}
|
||||||
|
} catch (extractError) {
|
||||||
|
console.warn('Failed to extract files for preview:', extractError);
|
||||||
|
}
|
||||||
|
}, [addFiles]);
|
||||||
|
|
||||||
|
const executeOperation = useCallback(async (
|
||||||
|
mode: SplitMode | '',
|
||||||
|
parameters: SplitParameters,
|
||||||
|
selectedFiles: File[]
|
||||||
|
) => {
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
setStatus(t("noFileSelected"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { operation, operationId, fileId } = createOperation(mode, parameters, selectedFiles);
|
||||||
|
const { formData, endpoint } = buildFormData(mode, parameters, selectedFiles);
|
||||||
|
|
||||||
|
recordOperation(fileId, operation);
|
||||||
|
|
||||||
|
setStatus(t("loading"));
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||||
|
const blob = new Blob([response.data], { type: "application/zip" });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
setDownloadUrl(url);
|
||||||
|
setStatus(t("downloadComplete"));
|
||||||
|
|
||||||
|
await processResults(blob);
|
||||||
|
markOperationApplied(fileId, operationId);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
let errorMsg = t("error.pdfPassword", "An error occurred while splitting the PDF.");
|
||||||
|
if (error.response?.data && typeof error.response.data === 'string') {
|
||||||
|
errorMsg = error.response.data;
|
||||||
|
} else if (error.message) {
|
||||||
|
errorMsg = error.message;
|
||||||
|
}
|
||||||
|
setErrorMessage(errorMsg);
|
||||||
|
setStatus(t("error._value", "Split failed."));
|
||||||
|
markOperationFailed(fileId, operationId, errorMsg);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [t, createOperation, buildFormData, recordOperation, markOperationApplied, markOperationFailed, processResults]);
|
||||||
|
|
||||||
|
const resetResults = useCallback(() => {
|
||||||
|
setFiles([]);
|
||||||
|
setThumbnails([]);
|
||||||
|
setIsGeneratingThumbnails(false);
|
||||||
|
setDownloadUrl(null);
|
||||||
|
setStatus('');
|
||||||
|
setErrorMessage(null);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearError = useCallback(() => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
executeOperation,
|
||||||
|
|
||||||
|
// Flattened result properties for cleaner access
|
||||||
|
files,
|
||||||
|
thumbnails,
|
||||||
|
isGeneratingThumbnails,
|
||||||
|
downloadUrl,
|
||||||
|
status,
|
||||||
|
errorMessage,
|
||||||
|
isLoading,
|
||||||
|
|
||||||
|
// Result management functions
|
||||||
|
resetResults,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
};
|
71
frontend/src/hooks/tools/split/useSplitParameters.ts
Normal file
71
frontend/src/hooks/tools/split/useSplitParameters.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { SPLIT_MODES, SPLIT_TYPES, ENDPOINTS, type SplitMode, type SplitType } from '../../../constants/splitConstants';
|
||||||
|
import { SplitParameters } from '../../../components/tools/split/SplitSettings';
|
||||||
|
|
||||||
|
export interface SplitParametersHook {
|
||||||
|
mode: SplitMode | '';
|
||||||
|
parameters: SplitParameters;
|
||||||
|
setMode: (mode: SplitMode | '') => void;
|
||||||
|
updateParameter: (parameter: keyof SplitParameters, value: string | boolean) => void;
|
||||||
|
resetParameters: () => void;
|
||||||
|
validateParameters: () => boolean;
|
||||||
|
getEndpointName: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialParameters: SplitParameters = {
|
||||||
|
pages: '',
|
||||||
|
hDiv: '2',
|
||||||
|
vDiv: '2',
|
||||||
|
merge: false,
|
||||||
|
splitType: SPLIT_TYPES.SIZE,
|
||||||
|
splitValue: '',
|
||||||
|
bookmarkLevel: '1',
|
||||||
|
includeMetadata: false,
|
||||||
|
allowDuplicates: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSplitParameters = (): SplitParametersHook => {
|
||||||
|
const [mode, setMode] = useState<SplitMode | ''>('');
|
||||||
|
const [parameters, setParameters] = useState<SplitParameters>(initialParameters);
|
||||||
|
|
||||||
|
const updateParameter = (parameter: keyof SplitParameters, value: string | boolean) => {
|
||||||
|
setParameters(prev => ({ ...prev, [parameter]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetParameters = () => {
|
||||||
|
setParameters(initialParameters);
|
||||||
|
setMode('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateParameters = () => {
|
||||||
|
if (!mode) return false;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case SPLIT_MODES.BY_PAGES:
|
||||||
|
return parameters.pages.trim() !== "";
|
||||||
|
case SPLIT_MODES.BY_SECTIONS:
|
||||||
|
return parameters.hDiv !== "" && parameters.vDiv !== "";
|
||||||
|
case SPLIT_MODES.BY_SIZE_OR_COUNT:
|
||||||
|
return parameters.splitValue.trim() !== "";
|
||||||
|
case SPLIT_MODES.BY_CHAPTERS:
|
||||||
|
return parameters.bookmarkLevel !== "";
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEndpointName = () => {
|
||||||
|
if (!mode) return ENDPOINTS[SPLIT_MODES.BY_PAGES];
|
||||||
|
return ENDPOINTS[mode as SplitMode];
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
parameters,
|
||||||
|
setMode,
|
||||||
|
updateParameter,
|
||||||
|
resetParameters,
|
||||||
|
validateParameters,
|
||||||
|
getEndpointName,
|
||||||
|
};
|
||||||
|
};
|
@ -1,562 +1,163 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import axios from "axios";
|
import { Button, Stack, Text } from "@mantine/core";
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Select,
|
|
||||||
TextInput,
|
|
||||||
Checkbox,
|
|
||||||
Notification,
|
|
||||||
Stack,
|
|
||||||
Paper,
|
|
||||||
Text,
|
|
||||||
Alert,
|
|
||||||
Box,
|
|
||||||
Group,
|
|
||||||
Grid,
|
|
||||||
Image,
|
|
||||||
Loader,
|
|
||||||
Center,
|
|
||||||
} 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 { useFileContext } from "../contexts/FileContext";
|
|
||||||
import { FileOperation } from "../types/fileContext";
|
|
||||||
import { zipFileService } from "../services/zipFileService";
|
|
||||||
import { generateThumbnailForFile } from "../utils/thumbnailUtils";
|
|
||||||
import FileEditor from "../components/fileEditor/FileEditor";
|
|
||||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||||
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
|
|
||||||
export interface SplitPdfPanelProps {
|
import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
|
||||||
|
import OperationButton from "../components/tools/shared/OperationButton";
|
||||||
|
import ErrorNotification from "../components/tools/shared/ErrorNotification";
|
||||||
|
import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
|
||||||
|
import ResultsPreview from "../components/tools/shared/ResultsPreview";
|
||||||
|
|
||||||
|
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||||
|
|
||||||
|
import { useSplitParameters } from "../hooks/tools/split/useSplitParameters";
|
||||||
|
import { useSplitOperation } from "../hooks/tools/split/useSplitOperation";
|
||||||
|
|
||||||
|
interface SplitProps {
|
||||||
selectedFiles?: File[];
|
selectedFiles?: File[];
|
||||||
onPreviewFile?: (file: File | null) => void;
|
onPreviewFile?: (file: File | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
const Split = ({ selectedFiles = [], onPreviewFile }: SplitProps) => {
|
||||||
selectedFiles = [],
|
|
||||||
onPreviewFile,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fileContext = useFileContext();
|
const { setCurrentMode } = useFileContext();
|
||||||
const { activeFiles, selectedFileIds, updateProcessedFile, recordOperation, markOperationApplied, markOperationFailed, setCurrentMode } = fileContext;
|
|
||||||
|
|
||||||
// Internal split parameter state
|
const splitParams = useSplitParameters();
|
||||||
const [mode, setMode] = useState('');
|
const splitOperation = useSplitOperation();
|
||||||
const [pages, setPages] = useState('');
|
|
||||||
const [hDiv, setHDiv] = useState('2');
|
|
||||||
const [vDiv, setVDiv] = useState('2');
|
|
||||||
const [merge, setMerge] = useState(false);
|
|
||||||
const [splitType, setSplitType] = useState('size');
|
|
||||||
const [splitValue, setSplitValue] = useState('');
|
|
||||||
const [bookmarkLevel, setBookmarkLevel] = useState('1');
|
|
||||||
const [includeMetadata, setIncludeMetadata] = useState(false);
|
|
||||||
const [allowDuplicates, setAllowDuplicates] = useState(false);
|
|
||||||
|
|
||||||
// Helper to get endpoint name from split mode
|
// Endpoint validation
|
||||||
const getEndpointName = (mode: string) => {
|
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(
|
||||||
switch (mode) {
|
splitParams.getEndpointName()
|
||||||
case "byPages":
|
);
|
||||||
return "split-pages";
|
|
||||||
case "bySections":
|
|
||||||
return "split-pdf-by-sections";
|
|
||||||
case "bySizeOrCount":
|
|
||||||
return "split-by-size-or-count";
|
|
||||||
case "byChapters":
|
|
||||||
return "split-pdf-by-chapters";
|
|
||||||
default:
|
|
||||||
return "split-pages"; // Default fallback
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [status, setStatus] = useState("");
|
useEffect(() => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
splitOperation.resetResults();
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
||||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
|
||||||
const [splitResults, setSplitResults] = useState<{
|
|
||||||
files: File[];
|
|
||||||
thumbnails: string[];
|
|
||||||
isGeneratingThumbnails: boolean;
|
|
||||||
}>({
|
|
||||||
files: [],
|
|
||||||
thumbnails: [],
|
|
||||||
isGeneratingThumbnails: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear download when parameters or files change
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (downloadUrl) {
|
|
||||||
setDownloadUrl(null);
|
|
||||||
setStatus("");
|
|
||||||
}
|
|
||||||
// Clear split results
|
|
||||||
setSplitResults({
|
|
||||||
files: [],
|
|
||||||
thumbnails: [],
|
|
||||||
isGeneratingThumbnails: false
|
|
||||||
});
|
|
||||||
onPreviewFile?.(null);
|
onPreviewFile?.(null);
|
||||||
}, [mode, pages, hDiv, vDiv, merge, splitType, splitValue, bookmarkLevel, includeMetadata, allowDuplicates, selectedFiles]);
|
}, [splitParams.mode, splitParams.parameters, selectedFiles]);
|
||||||
|
|
||||||
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(getEndpointName(mode));
|
const handleSplit = async () => {
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
await splitOperation.executeOperation(
|
||||||
e.preventDefault();
|
splitParams.mode,
|
||||||
if (selectedFiles.length === 0) {
|
splitParams.parameters,
|
||||||
setStatus(t("noFileSelected"));
|
selectedFiles
|
||||||
return;
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
// Use selected files from context
|
|
||||||
selectedFiles.forEach(file => {
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
});
|
|
||||||
|
|
||||||
let endpoint = "";
|
|
||||||
|
|
||||||
switch (mode) {
|
|
||||||
case "byPages":
|
|
||||||
formData.append("pageNumbers", pages);
|
|
||||||
endpoint = "/api/v1/general/split-pages";
|
|
||||||
break;
|
|
||||||
case "bySections":
|
|
||||||
formData.append("horizontalDivisions", hDiv);
|
|
||||||
formData.append("verticalDivisions", vDiv);
|
|
||||||
formData.append("merge", merge.toString());
|
|
||||||
endpoint = "/api/v1/general/split-pdf-by-sections";
|
|
||||||
break;
|
|
||||||
case "bySizeOrCount":
|
|
||||||
formData.append(
|
|
||||||
"splitType",
|
|
||||||
splitType === "size" ? "0" : splitType === "pages" ? "1" : "2"
|
|
||||||
);
|
|
||||||
formData.append("splitValue", splitValue);
|
|
||||||
endpoint = "/api/v1/general/split-by-size-or-count";
|
|
||||||
break;
|
|
||||||
case "byChapters":
|
|
||||||
formData.append("bookmarkLevel", bookmarkLevel);
|
|
||||||
formData.append("includeMetadata", includeMetadata.toString());
|
|
||||||
formData.append("allowDuplicates", allowDuplicates.toString());
|
|
||||||
endpoint = "/api/v1/general/split-pdf-by-chapters";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record the operation before starting
|
|
||||||
const operationId = `split-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
const fileId = selectedFiles[0].name; // Use first file's name as primary ID
|
|
||||||
|
|
||||||
const operation: FileOperation = {
|
|
||||||
id: operationId,
|
|
||||||
type: 'split',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
fileIds: selectedFiles.map(f => f.name),
|
|
||||||
status: 'pending',
|
|
||||||
metadata: {
|
|
||||||
originalFileName: selectedFiles[0].name,
|
|
||||||
parameters: {
|
|
||||||
mode,
|
|
||||||
pages: mode === 'byPages' ? pages : undefined,
|
|
||||||
hDiv: mode === 'bySections' ? hDiv : undefined,
|
|
||||||
vDiv: mode === 'bySections' ? vDiv : undefined,
|
|
||||||
merge: mode === 'bySections' ? merge : undefined,
|
|
||||||
splitType: mode === 'bySizeOrCount' ? splitType : undefined,
|
|
||||||
splitValue: mode === 'bySizeOrCount' ? splitValue : undefined,
|
|
||||||
bookmarkLevel: mode === 'byChapters' ? bookmarkLevel : undefined,
|
|
||||||
includeMetadata: mode === 'byChapters' ? includeMetadata : undefined,
|
|
||||||
allowDuplicates: mode === 'byChapters' ? allowDuplicates : undefined,
|
|
||||||
},
|
|
||||||
fileSize: selectedFiles[0].size
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
recordOperation(fileId, operation);
|
|
||||||
|
|
||||||
setStatus(t("loading"));
|
|
||||||
setIsLoading(true);
|
|
||||||
setErrorMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
|
||||||
const blob = new Blob([response.data], { type: "application/zip" });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
setDownloadUrl(url);
|
|
||||||
setStatus(t("downloadComplete"));
|
|
||||||
|
|
||||||
// Extract files from ZIP response for preview
|
|
||||||
try {
|
|
||||||
// Create a File object from the blob to use with zipFileService
|
|
||||||
const zipFile = new File([blob], "split_result.zip", { type: "application/zip" });
|
|
||||||
|
|
||||||
// Extract PDF files for preview
|
|
||||||
const extractionResult = await zipFileService.extractPdfFiles(zipFile);
|
|
||||||
|
|
||||||
if (extractionResult.success && extractionResult.extractedFiles.length > 0) {
|
|
||||||
setSplitResults(prev => ({
|
|
||||||
...prev,
|
|
||||||
files: extractionResult.extractedFiles,
|
|
||||||
isGeneratingThumbnails: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Generate thumbnails for preview
|
|
||||||
const thumbnails = await Promise.all(
|
|
||||||
extractionResult.extractedFiles.map(async (file) => {
|
|
||||||
try {
|
|
||||||
return await generateThumbnailForFile(file);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
|
||||||
return ''; // Empty string for failed thumbnails
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setSplitResults(prev => ({
|
|
||||||
...prev,
|
|
||||||
thumbnails,
|
|
||||||
isGeneratingThumbnails: false
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} catch (extractError) {
|
|
||||||
console.warn('Failed to extract files for preview:', extractError);
|
|
||||||
// Don't fail the whole operation just because preview extraction failed
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark operation as applied on success
|
|
||||||
markOperationApplied(fileId, operationId);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
let errorMsg = t("error.pdfPassword", "An error occurred while splitting the PDF.");
|
|
||||||
if (error.response?.data && typeof error.response.data === 'string') {
|
|
||||||
errorMsg = error.response.data;
|
|
||||||
} else if (error.message) {
|
|
||||||
errorMsg = error.message;
|
|
||||||
}
|
|
||||||
setErrorMessage(errorMsg);
|
|
||||||
setStatus(t("error._value", "Split failed."));
|
|
||||||
|
|
||||||
// Mark operation as failed
|
|
||||||
markOperationFailed(fileId, operationId, errorMsg);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if current mode needs additional parameters
|
|
||||||
const modeNeedsParams = (currentMode: string) => {
|
|
||||||
return currentMode && currentMode !== ""; // All modes need some params
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle thumbnail click to open in viewer
|
|
||||||
const handleThumbnailClick = (file: File) => {
|
const handleThumbnailClick = (file: File) => {
|
||||||
try {
|
onPreviewFile?.(file);
|
||||||
onPreviewFile?.(file);
|
sessionStorage.setItem('previousMode', 'split');
|
||||||
sessionStorage.setItem('previousMode', 'split');
|
setCurrentMode('viewer');
|
||||||
setCurrentMode('viewer');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to open file in viewer:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// No longer needed - step completion is determined by split results
|
const handleSettingsReset = () => {
|
||||||
|
splitOperation.resetResults();
|
||||||
|
onPreviewFile?.(null);
|
||||||
|
setCurrentMode('split');
|
||||||
|
};
|
||||||
|
|
||||||
// Check if step 2 settings are valid (for enabling Split button)
|
const hasFiles = selectedFiles.length > 0;
|
||||||
const step2SettingsValid = (() => {
|
const hasResults = splitOperation.downloadUrl !== null;
|
||||||
if (!mode) return false;
|
const filesCollapsed = hasFiles;
|
||||||
|
const settingsCollapsed = hasResults;
|
||||||
|
|
||||||
switch (mode) {
|
const previewResults = useMemo(() =>
|
||||||
case "byPages":
|
splitOperation.files?.map((file, index) => ({
|
||||||
return pages.trim() !== "";
|
file,
|
||||||
case "bySections":
|
thumbnail: splitOperation.thumbnails[index]
|
||||||
return hDiv !== "" && vDiv !== "";
|
})) || [],
|
||||||
case "bySizeOrCount":
|
[splitOperation.files, splitOperation.thumbnails]
|
||||||
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 = downloadUrl !== null; // Review (show results after split)
|
|
||||||
|
|
||||||
// Determine if steps are collapsed (completed)
|
|
||||||
const step1Collapsed = selectedFiles.length > 0;
|
|
||||||
const step2Collapsed = downloadUrl !== null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box h="100%" p="md" style={{ overflow: 'auto' }}>
|
<ToolStepContainer>
|
||||||
<Stack gap="md">
|
<Stack gap="md" h="100%" p="md" style={{ overflow: 'auto' }}>
|
||||||
{/* Step 1: Files */}
|
{/* Files Step */}
|
||||||
{showStep1 && (
|
<ToolStep
|
||||||
<Paper p="md" withBorder>
|
title="Files"
|
||||||
<Text fw={500} size="lg" mb="sm">1. Files</Text>
|
isVisible={true}
|
||||||
{step1Collapsed ? (
|
isCollapsed={filesCollapsed}
|
||||||
<Text size="sm" c="green">
|
isCompleted={filesCollapsed}
|
||||||
✓ Selected: {selectedFiles[0]?.name}
|
completedMessage={hasFiles ? `Selected: ${selectedFiles[0]?.name}` : undefined}
|
||||||
</Text>
|
>
|
||||||
) : (
|
<FileStatusIndicator
|
||||||
<Text size="sm" c="dimmed">
|
selectedFiles={selectedFiles}
|
||||||
Select a PDF file in the main view to get started
|
placeholder="Select a PDF file in the main view to get started"
|
||||||
</Text>
|
/>
|
||||||
|
</ToolStep>
|
||||||
|
|
||||||
|
{/* Settings Step */}
|
||||||
|
<ToolStep
|
||||||
|
title="Settings"
|
||||||
|
isVisible={hasFiles}
|
||||||
|
isCollapsed={settingsCollapsed}
|
||||||
|
isCompleted={settingsCollapsed}
|
||||||
|
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||||
|
completedMessage={settingsCollapsed ? "Split completed" : undefined}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<SplitSettings
|
||||||
|
mode={splitParams.mode}
|
||||||
|
onModeChange={splitParams.setMode}
|
||||||
|
parameters={splitParams.parameters}
|
||||||
|
onParameterChange={splitParams.updateParameter}
|
||||||
|
disabled={endpointLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{splitParams.mode && (
|
||||||
|
<OperationButton
|
||||||
|
onClick={handleSplit}
|
||||||
|
isLoading={splitOperation.isLoading}
|
||||||
|
disabled={!splitParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||||
|
loadingText={t("loading")}
|
||||||
|
submitText={t("split.submit", "Split PDF")}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Stack>
|
||||||
)}
|
</ToolStep>
|
||||||
|
|
||||||
{/* Step 2: Settings */}
|
{/* Results Step */}
|
||||||
{showStep2 && (
|
<ToolStep
|
||||||
<Paper
|
title="Results"
|
||||||
p="md"
|
isVisible={hasResults}
|
||||||
withBorder
|
>
|
||||||
style={{
|
<Stack gap="md">
|
||||||
cursor: step2Collapsed ? 'pointer' : 'default',
|
{splitOperation.status && (
|
||||||
opacity: step2Collapsed ? 0.8 : 1,
|
<Text size="sm" c="dimmed">{splitOperation.status}</Text>
|
||||||
transition: 'opacity 0.2s ease'
|
|
||||||
}}
|
|
||||||
onClick={step2Collapsed ? () => {
|
|
||||||
// Reset to allow changing settings
|
|
||||||
setDownloadUrl(null);
|
|
||||||
setSplitResults({
|
|
||||||
files: [],
|
|
||||||
thumbnails: [],
|
|
||||||
isGeneratingThumbnails: false
|
|
||||||
});
|
|
||||||
setStatus("");
|
|
||||||
setErrorMessage(null);
|
|
||||||
// Clear any active preview and return to previous view
|
|
||||||
onPreviewFile?.(null);
|
|
||||||
setCurrentMode('split');
|
|
||||||
} : undefined}
|
|
||||||
>
|
|
||||||
<Text fw={500} size="lg" mb="sm">2. Settings</Text>
|
|
||||||
{step2Collapsed ? (
|
|
||||||
<Text size="sm" c="green">
|
|
||||||
✓ Split completed <Text span c="dimmed" size="xs">(click to change settings)</Text>
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Stack gap="md">
|
|
||||||
<Select
|
|
||||||
label="Choose split method"
|
|
||||||
placeholder="Select how to split the PDF"
|
|
||||||
value={mode}
|
|
||||||
onChange={(v) => v && setMode(v)}
|
|
||||||
data={[
|
|
||||||
{ value: "byPages", label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" },
|
|
||||||
{ value: "bySections", label: t("split-by-sections.title", "Split by Grid Sections") },
|
|
||||||
{ 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-specific Parameters */}
|
|
||||||
{mode === "byPages" && (
|
|
||||||
<TextInput
|
|
||||||
label={t("split.splitPages", "Pages")}
|
|
||||||
placeholder={t("pageSelectionPrompt", "e.g. 1,3,5-10")}
|
|
||||||
value={pages}
|
|
||||||
onChange={(e) => setPages(e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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) => setHDiv(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) => setVDiv(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) => setMerge(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 && setSplitType(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) => setSplitValue(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{mode === "byChapters" && (
|
|
||||||
<Stack gap="sm">
|
|
||||||
<TextInput
|
|
||||||
label={t("splitByChapters.bookmarkLevel", "Bookmark Level")}
|
|
||||||
type="number"
|
|
||||||
value={bookmarkLevel}
|
|
||||||
onChange={(e) => setBookmarkLevel(e.target.value)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t("splitByChapters.includeMetadata", "Include Metadata")}
|
|
||||||
checked={includeMetadata}
|
|
||||||
onChange={(e) => setIncludeMetadata(e.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={t("splitByChapters.allowDuplicates", "Allow Duplicate Bookmarks")}
|
|
||||||
checked={allowDuplicates}
|
|
||||||
onChange={(e) => setAllowDuplicates(e.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Split Button */}
|
|
||||||
{mode && (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
fullWidth
|
|
||||||
mt="md"
|
|
||||||
loading={isLoading}
|
|
||||||
disabled={!step2SettingsValid || selectedFiles.length === 0}
|
|
||||||
>
|
|
||||||
{isLoading ? t("loading") : t("split.submit", "Split PDF")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 3: Results */}
|
|
||||||
{showStep3 && (
|
|
||||||
<Paper p="md" withBorder>
|
|
||||||
<Text fw={500} size="lg" mb="sm">3. Results</Text>
|
|
||||||
|
|
||||||
{status && <Text size="sm" c="dimmed" mb="md">{status}</Text>}
|
|
||||||
|
|
||||||
{errorMessage && (
|
|
||||||
<Notification color="red" title={t("error._value", "Error")} onClose={() => setErrorMessage(null)} mb="md">
|
|
||||||
{errorMessage}
|
|
||||||
</Notification>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{downloadUrl && (
|
<ErrorNotification
|
||||||
|
error={splitOperation.errorMessage}
|
||||||
|
onClose={splitOperation.clearError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{splitOperation.downloadUrl && (
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
href={downloadUrl}
|
href={splitOperation.downloadUrl}
|
||||||
download="split_output.zip"
|
download="split_output.zip"
|
||||||
leftSection={<DownloadIcon />}
|
leftSection={<DownloadIcon />}
|
||||||
color="green"
|
color="green"
|
||||||
fullWidth
|
fullWidth
|
||||||
mb="md"
|
mb="md"
|
||||||
>
|
>
|
||||||
|
{t("download", "Download")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Split Results Preview */}
|
<ResultsPreview
|
||||||
{(splitResults.files.length > 0 || splitResults.isGeneratingThumbnails) && (
|
files={previewResults}
|
||||||
<Box mt="lg" p="md" style={{ backgroundColor: 'var(--mantine-color-gray-0)', borderRadius: 8 }}>
|
onFileClick={handleThumbnailClick}
|
||||||
<Text fw={500} size="md" mb="sm">
|
isGeneratingThumbnails={splitOperation.isGeneratingThumbnails}
|
||||||
Split Results ({splitResults.files.length} files)
|
title="Split Results"
|
||||||
</Text>
|
/>
|
||||||
|
</Stack>
|
||||||
{splitResults.isGeneratingThumbnails ? (
|
</ToolStep>
|
||||||
<Center p="lg">
|
|
||||||
<Stack align="center" gap="sm">
|
|
||||||
<Loader size="sm" />
|
|
||||||
<Text size="sm" c="dimmed">Generating previews...</Text>
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
) : (
|
|
||||||
<Grid>
|
|
||||||
{splitResults.files.map((file, index) => (
|
|
||||||
<Grid.Col span={{ base: 6, sm: 4, md: 3 }} key={index}>
|
|
||||||
<Paper
|
|
||||||
p="xs"
|
|
||||||
withBorder
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
height: '200px',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
cursor: 'pointer',
|
|
||||||
transition: 'all 0.2s ease'
|
|
||||||
}}
|
|
||||||
onClick={() => handleThumbnailClick(file)}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.transform = 'scale(1.02)';
|
|
||||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.transform = 'scale(1)';
|
|
||||||
e.currentTarget.style.boxShadow = '';
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
{splitResults.thumbnails[index] ? (
|
|
||||||
<Image
|
|
||||||
src={splitResults.thumbnails[index]}
|
|
||||||
alt={`Preview of ${file.name}`}
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
maxHeight: '140px',
|
|
||||||
objectFit: 'contain'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Text size="xs" c="dimmed">No preview</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
c="dimmed"
|
|
||||||
mt="xs"
|
|
||||||
style={{
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}}
|
|
||||||
title={file.name}
|
|
||||||
>
|
|
||||||
{file.name}
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="dimmed">
|
|
||||||
{(file.size / 1024).toFixed(1)} KB
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</ToolStepContainer>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default SplitPdfPanel;
|
export default Split;
|
||||||
|
Loading…
Reference in New Issue
Block a user