mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
Stirling 2.0 (#3928)
# Description of Changes <!-- File context for managing files between tools and views Optimisation for large files Updated Split to work with new file system and match Matts stepped design closer --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This commit is contained in:
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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user