mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
Enforce type checking in CI (#4126)
# Description of Changes Currently, the `tsconfig.json` file enforces strict type checking, but nothing in CI checks that the code is actually correctly typed. [Vite only transpiles TypeScript code](https://vite.dev/guide/features.html#transpile-only) so doesn't ensure that the TS code we're running is correct. This PR adds running of the type checker to CI and fixes the type errors that have already crept into the codebase. Note that many of the changes I've made to 'fix the types' are just using `any` to disable the type checker because the code is under too much churn to fix anything properly at the moment. I still think enabling the type checker now is the best course of action though because otherwise we'll never be able to fix all of them, and it should at least help us not break things when adding new code. Co-authored-by: James <[email protected]>
This commit is contained in:
@@ -70,11 +70,11 @@ const FileEditor = ({
|
||||
} = fileContext;
|
||||
|
||||
// Get file selection context
|
||||
const {
|
||||
selectedFiles: toolSelectedFiles,
|
||||
setSelectedFiles: setToolSelectedFiles,
|
||||
maxFiles,
|
||||
isToolMode
|
||||
const {
|
||||
selectedFiles: toolSelectedFiles,
|
||||
setSelectedFiles: setToolSelectedFiles,
|
||||
maxFiles,
|
||||
isToolMode
|
||||
} = useFileSelection();
|
||||
|
||||
const [files, setFiles] = useState<FileItem[]>([]);
|
||||
@@ -82,7 +82,7 @@ const FileEditor = ({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [localLoading, setLocalLoading] = useState(false);
|
||||
const [selectionMode, setSelectionMode] = useState(toolMode);
|
||||
|
||||
|
||||
// Enable selection mode automatically in tool mode
|
||||
React.useEffect(() => {
|
||||
if (toolMode) {
|
||||
@@ -115,7 +115,7 @@ const FileEditor = ({
|
||||
|
||||
// Get selected file IDs from context (defensive programming)
|
||||
const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : [];
|
||||
|
||||
|
||||
// Map context selections to local file IDs for UI display
|
||||
const localSelectedIds = files
|
||||
.filter(file => {
|
||||
@@ -144,33 +144,33 @@ const FileEditor = ({
|
||||
// Check if the actual content has changed, not just references
|
||||
const currentActiveFileNames = activeFiles.map(f => f.name);
|
||||
const currentProcessedFilesSize = processedFiles.size;
|
||||
|
||||
|
||||
const activeFilesChanged = JSON.stringify(currentActiveFileNames) !== JSON.stringify(lastActiveFilesRef.current);
|
||||
const processedFilesChanged = currentProcessedFilesSize !== lastProcessedFilesRef.current;
|
||||
|
||||
|
||||
if (!activeFilesChanged && !processedFilesChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Update refs
|
||||
lastActiveFilesRef.current = currentActiveFileNames;
|
||||
lastProcessedFilesRef.current = currentProcessedFilesSize;
|
||||
|
||||
|
||||
const convertActiveFiles = async () => {
|
||||
|
||||
|
||||
if (activeFiles.length > 0) {
|
||||
setLocalLoading(true);
|
||||
try {
|
||||
// Process files in chunks to avoid blocking UI
|
||||
const convertedFiles: FileItem[] = [];
|
||||
|
||||
|
||||
for (let i = 0; i < activeFiles.length; i++) {
|
||||
const file = activeFiles[i];
|
||||
|
||||
|
||||
// Try to get thumbnail from processed file first
|
||||
const processedFile = processedFiles.get(file);
|
||||
let thumbnail = processedFile?.pages?.[0]?.thumbnail;
|
||||
|
||||
|
||||
// If no thumbnail from processed file, try to generate one
|
||||
if (!thumbnail) {
|
||||
try {
|
||||
@@ -180,28 +180,28 @@ const FileEditor = ({
|
||||
thumbnail = undefined; // Use placeholder
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const convertedFile = {
|
||||
id: `file-${Date.now()}-${Math.random()}`,
|
||||
name: file.name,
|
||||
pageCount: processedFile?.totalPages || Math.floor(Math.random() * 20) + 1,
|
||||
thumbnail,
|
||||
thumbnail: thumbnail || '',
|
||||
size: file.size,
|
||||
file,
|
||||
};
|
||||
|
||||
|
||||
convertedFiles.push(convertedFile);
|
||||
|
||||
|
||||
// Update progress
|
||||
setConversionProgress(((i + 1) / activeFiles.length) * 100);
|
||||
|
||||
|
||||
// Yield to main thread between files
|
||||
if (i < activeFiles.length - 1) {
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
setFiles(convertedFiles);
|
||||
} catch (err) {
|
||||
console.error('Error converting active files:', err);
|
||||
@@ -237,7 +237,7 @@ const FileEditor = ({
|
||||
try {
|
||||
// Validate ZIP file first
|
||||
const validation = await zipFileService.validateZipFile(file);
|
||||
|
||||
|
||||
if (validation.isValid && validation.containsPDFs) {
|
||||
// ZIP contains PDFs - extract them
|
||||
setZipExtractionProgress({
|
||||
@@ -269,7 +269,7 @@ const FileEditor = ({
|
||||
|
||||
if (extractionResult.success) {
|
||||
allExtractedFiles.push(...extractionResult.extractedFiles);
|
||||
|
||||
|
||||
// Record ZIP extraction operation
|
||||
const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const operation: FileOperation = {
|
||||
@@ -289,10 +289,10 @@ const FileEditor = ({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
recordOperation(file.name, operation);
|
||||
markOperationApplied(file.name, operationId);
|
||||
|
||||
|
||||
if (extractionResult.errors.length > 0) {
|
||||
errors.push(...extractionResult.errors);
|
||||
}
|
||||
@@ -344,7 +344,7 @@ const FileEditor = ({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
recordOperation(file.name, operation);
|
||||
markOperationApplied(file.name, operationId);
|
||||
}
|
||||
@@ -357,7 +357,7 @@ const FileEditor = ({
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to process files';
|
||||
setError(errorMessage);
|
||||
console.error('File processing error:', err);
|
||||
|
||||
|
||||
// Reset extraction progress on error
|
||||
setZipExtractionProgress({
|
||||
isExtracting: false,
|
||||
@@ -377,7 +377,7 @@ const FileEditor = ({
|
||||
|
||||
const closeAllFiles = useCallback(() => {
|
||||
if (activeFiles.length === 0) return;
|
||||
|
||||
|
||||
// Record close all operation for each file
|
||||
activeFiles.forEach(file => {
|
||||
const operationId = `close-all-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
@@ -396,14 +396,14 @@ const FileEditor = ({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
recordOperation(file.name, operation);
|
||||
markOperationApplied(file.name, operationId);
|
||||
});
|
||||
|
||||
|
||||
// Remove all files from context but keep in storage
|
||||
removeFiles(activeFiles.map(f => (f as any).id || f.name), false);
|
||||
|
||||
|
||||
// Clear selections
|
||||
setContextSelectedFiles([]);
|
||||
}, [activeFiles, removeFiles, setContextSelectedFiles, recordOperation, markOperationApplied]);
|
||||
@@ -411,12 +411,12 @@ const FileEditor = ({
|
||||
const toggleFile = useCallback((fileId: string) => {
|
||||
const targetFile = files.find(f => f.id === fileId);
|
||||
if (!targetFile) return;
|
||||
|
||||
|
||||
const contextFileId = (targetFile.file as any).id || targetFile.name;
|
||||
const isSelected = contextSelectedIds.includes(contextFileId);
|
||||
|
||||
|
||||
let newSelection: string[];
|
||||
|
||||
|
||||
if (isSelected) {
|
||||
// Remove file from selection
|
||||
newSelection = contextSelectedIds.filter(id => id !== contextFileId);
|
||||
@@ -433,10 +433,10 @@ const FileEditor = ({
|
||||
newSelection = [...contextSelectedIds, contextFileId];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Update context
|
||||
setContextSelectedFiles(newSelection);
|
||||
|
||||
|
||||
// Update tool selection context if in tool mode
|
||||
if (isToolMode || toolMode) {
|
||||
const selectedFiles = files
|
||||
@@ -572,12 +572,12 @@ const FileEditor = ({
|
||||
console.log('handleDeleteFile called with fileId:', fileId);
|
||||
const file = files.find(f => f.id === fileId);
|
||||
console.log('Found file:', file);
|
||||
|
||||
|
||||
if (file) {
|
||||
console.log('Attempting to remove file:', file.name);
|
||||
console.log('Actual file object:', file.file);
|
||||
console.log('Actual file.file.name:', file.file.name);
|
||||
|
||||
|
||||
// Record close operation
|
||||
const fileName = file.file.name;
|
||||
const fileId = (file.file as any).id || fileName;
|
||||
@@ -597,19 +597,16 @@ const FileEditor = ({
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
recordOperation(fileName, operation);
|
||||
|
||||
|
||||
// Remove file from context but keep in storage (close, don't delete)
|
||||
console.log('Calling removeFiles with:', [fileId]);
|
||||
removeFiles([fileId], false);
|
||||
|
||||
|
||||
// Remove from context selections
|
||||
setContextSelectedFiles(prev => {
|
||||
const safePrev = Array.isArray(prev) ? prev : [];
|
||||
return safePrev.filter(id => id !== fileId);
|
||||
});
|
||||
|
||||
const newSelection = contextSelectedIds.filter(id => id !== fileId);
|
||||
setContextSelectedFiles(newSelection);
|
||||
// Mark operation as applied
|
||||
markOperationApplied(fileName, operationId);
|
||||
} else {
|
||||
@@ -670,7 +667,7 @@ const FileEditor = ({
|
||||
accept={["*/*"]}
|
||||
multiple={true}
|
||||
maxSize={2 * 1024 * 1024 * 1024}
|
||||
style={{
|
||||
style={{
|
||||
height: '100vh',
|
||||
border: 'none',
|
||||
borderRadius: 0,
|
||||
@@ -707,7 +704,7 @@ const FileEditor = ({
|
||||
) : files.length === 0 && (localLoading || zipExtractionProgress.isExtracting) ? (
|
||||
<Box>
|
||||
<SkeletonLoader type="controls" />
|
||||
|
||||
|
||||
{/* ZIP Extraction Progress */}
|
||||
{zipExtractionProgress.isExtracting && (
|
||||
<Box mb="md" p="sm" style={{ backgroundColor: 'var(--mantine-color-orange-0)', borderRadius: 8 }}>
|
||||
@@ -721,10 +718,10 @@ const FileEditor = ({
|
||||
<Text size="xs" c="dimmed" mb="xs">
|
||||
{zipExtractionProgress.extractedCount} of {zipExtractionProgress.totalFiles} files extracted
|
||||
</Text>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
borderRadius: '2px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
@@ -737,7 +734,7 @@ const FileEditor = ({
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{/* Processing indicator */}
|
||||
{localLoading && (
|
||||
<Box mb="md" p="sm" style={{ backgroundColor: 'var(--mantine-color-blue-0)', borderRadius: 8 }}>
|
||||
@@ -745,10 +742,10 @@ const FileEditor = ({
|
||||
<Text size="sm" fw={500}>Loading files...</Text>
|
||||
<Text size="sm" c="dimmed">{Math.round(conversionProgress)}%</Text>
|
||||
</Group>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
borderRadius: '2px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
@@ -761,27 +758,27 @@ const FileEditor = ({
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
<SkeletonLoader type="fileGrid" count={6} />
|
||||
</Box>
|
||||
) : (
|
||||
<DragDropGrid
|
||||
items={files}
|
||||
selectedItems={localSelectedIds}
|
||||
selectedItems={localSelectedIds as any /* FIX ME */}
|
||||
selectionMode={selectionMode}
|
||||
isAnimating={isAnimating}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
onEndZoneDragEnter={handleEndZoneDragEnter}
|
||||
draggedItem={draggedFile}
|
||||
dropTarget={dropTarget}
|
||||
multiItemDrag={multiFileDrag}
|
||||
dragPosition={dragPosition}
|
||||
renderItem={(file, index, refs) => (
|
||||
onDragStart={handleDragStart as any /* FIX ME */}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragEnter as any /* FIX ME */}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop as any /* FIX ME */}
|
||||
onEndZoneDragEnter={handleEndZoneDragEnter}
|
||||
draggedItem={draggedFile as any /* FIX ME */}
|
||||
dropTarget={dropTarget as any /* FIX ME */}
|
||||
multiItemDrag={multiFileDrag as any /* FIX ME */}
|
||||
dragPosition={dragPosition}
|
||||
renderItem={(file, index, refs) => (
|
||||
<FileThumbnail
|
||||
file={file}
|
||||
index={index}
|
||||
@@ -801,8 +798,6 @@ const FileEditor = ({
|
||||
onToggleFile={toggleFile}
|
||||
onDeleteFile={handleDeleteFile}
|
||||
onViewFile={handleViewFile}
|
||||
onMergeFromHere={handleMergeFromHere}
|
||||
onSplitFile={handleSplitFile}
|
||||
onSetStatus={setStatus}
|
||||
toolMode={toolMode}
|
||||
isSupported={isFileSupported(file.name)}
|
||||
@@ -831,7 +826,6 @@ const FileEditor = ({
|
||||
onClose={() => setShowFilePickerModal(false)}
|
||||
storedFiles={[]} // FileEditor doesn't have access to stored files, needs to be passed from parent
|
||||
onSelectFiles={handleLoadFromStorage}
|
||||
allowMultiple={true}
|
||||
/>
|
||||
|
||||
{status && (
|
||||
|
||||
@@ -27,9 +27,10 @@ const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
|
||||
maxHeight = 400
|
||||
}) => {
|
||||
const { getFileHistory, getAppliedOperations } = useFileContext();
|
||||
|
||||
|
||||
const history = getFileHistory(fileId);
|
||||
const operations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || [];
|
||||
const allOperations = showOnlyApplied ? getAppliedOperations(fileId) : history?.operations || [];
|
||||
const operations = allOperations.filter(op => 'fileIds' in op) as FileOperation[];
|
||||
|
||||
const formatTimestamp = (timestamp: number) => {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
@@ -62,7 +63,7 @@ const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderOperationDetails = (operation: FileOperation | PageOperation) => {
|
||||
const renderOperationDetails = (operation: FileOperation) => {
|
||||
if ('metadata' in operation && operation.metadata) {
|
||||
const { metadata } = operation;
|
||||
return (
|
||||
@@ -142,7 +143,7 @@ const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
|
||||
<Badge
|
||||
variant="filled"
|
||||
color={getStatusColor(operation.status)}
|
||||
@@ -174,4 +175,4 @@ const FileOperationHistory: React.FC<FileOperationHistoryProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default FileOperationHistory;
|
||||
export default FileOperationHistory;
|
||||
|
||||
@@ -18,18 +18,18 @@ import LandingPage from '../shared/LandingPage';
|
||||
export default function Workbench() {
|
||||
const { t } = useTranslation();
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
|
||||
|
||||
// Use context-based hooks to eliminate all prop drilling
|
||||
const { activeFiles, currentView, setCurrentView } = useFileContext();
|
||||
const {
|
||||
previewFile,
|
||||
pageEditorFunctions,
|
||||
const {
|
||||
previewFile,
|
||||
pageEditorFunctions,
|
||||
sidebarsVisible,
|
||||
setPreviewFile,
|
||||
setPreviewFile,
|
||||
setPageEditorFunctions,
|
||||
setSidebarsVisible
|
||||
} = useWorkbenchState();
|
||||
|
||||
|
||||
const { selectedToolKey, selectedTool, handleToolSelect } = useToolSelection();
|
||||
const { addToActiveFiles } = useFileHandler();
|
||||
|
||||
@@ -142,10 +142,10 @@ export default function Workbench() {
|
||||
{/* Top Controls */}
|
||||
<TopControls
|
||||
currentView={currentView}
|
||||
setCurrentView={setCurrentView}
|
||||
setCurrentView={setCurrentView as any /* FIX ME */}
|
||||
selectedToolKey={selectedToolKey}
|
||||
/>
|
||||
|
||||
|
||||
{/* Main content area */}
|
||||
<Box
|
||||
className="flex-1 min-h-0 relative z-10"
|
||||
@@ -157,4 +157,4 @@ export default function Workbench() {
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ interface DragDropGridProps<T extends DragDropItem> {
|
||||
renderItem: (item: T, index: number, refs: React.MutableRefObject<Map<string, HTMLDivElement>>) => React.ReactNode;
|
||||
renderSplitMarker?: (item: T, index: number) => React.ReactNode;
|
||||
draggedItem: number | null;
|
||||
dropTarget: number | null;
|
||||
dropTarget: number | 'end' | null;
|
||||
multiItemDrag: {pageNumbers: number[], count: number} | null;
|
||||
dragPosition: {x: number, y: number} | null;
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ const FileThumbnail = ({
|
||||
onClose={() => setShowHistory(false)}
|
||||
title={`Operation History - ${file.name}`}
|
||||
size="lg"
|
||||
scrollAreaComponent="div"
|
||||
scrollAreaComponent={'div' as any}
|
||||
>
|
||||
<FileOperationHistory
|
||||
fileId={file.name}
|
||||
|
||||
@@ -43,7 +43,7 @@ export interface PageEditorProps {
|
||||
onExportAll: () => void;
|
||||
exportLoading: boolean;
|
||||
selectionMode: boolean;
|
||||
selectedPages: string[];
|
||||
selectedPages: number[];
|
||||
closePdf: () => void;
|
||||
}) => void;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ const PageEditor = ({
|
||||
// Get file context
|
||||
const fileContext = useFileContext();
|
||||
const { file: currentFile, processedFile: currentProcessedFile } = useCurrentFile();
|
||||
|
||||
|
||||
// Use file context state
|
||||
const {
|
||||
activeFiles,
|
||||
@@ -81,12 +81,12 @@ const PageEditor = ({
|
||||
// Simple computed document from processed files (no caching needed)
|
||||
const mergedPdfDocument = useMemo(() => {
|
||||
if (activeFiles.length === 0) return null;
|
||||
|
||||
|
||||
if (activeFiles.length === 1) {
|
||||
// Single file
|
||||
const processedFile = processedFiles.get(activeFiles[0]);
|
||||
if (!processedFile) return null;
|
||||
|
||||
|
||||
return {
|
||||
id: processedFile.id,
|
||||
name: activeFiles[0].name,
|
||||
@@ -108,7 +108,7 @@ const PageEditor = ({
|
||||
const processedFile = processedFiles.get(file);
|
||||
if (processedFile) {
|
||||
filenames.push(file.name.replace(/\.pdf$/i, ''));
|
||||
|
||||
|
||||
processedFile.pages.forEach((page, pageIndex) => {
|
||||
const newPage: PDFPage = {
|
||||
...page,
|
||||
@@ -119,7 +119,7 @@ const PageEditor = ({
|
||||
};
|
||||
allPages.push(newPage);
|
||||
});
|
||||
|
||||
|
||||
totalPages += processedFile.pages.length;
|
||||
}
|
||||
});
|
||||
@@ -140,7 +140,7 @@ const PageEditor = ({
|
||||
const displayDocument = editedDocument || mergedPdfDocument;
|
||||
|
||||
const [filename, setFilename] = useState<string>("");
|
||||
|
||||
|
||||
|
||||
// Page editor state (use context for selectedPages)
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
@@ -149,7 +149,7 @@ const PageEditor = ({
|
||||
|
||||
// Drag and drop state
|
||||
const [draggedPage, setDraggedPage] = useState<number | null>(null);
|
||||
const [dropTarget, setDropTarget] = useState<number | null>(null);
|
||||
const [dropTarget, setDropTarget] = useState<number | 'end' | null>(null);
|
||||
const [multiPageDrag, setMultiPageDrag] = useState<{pageNumbers: number[], count: number} | null>(null);
|
||||
const [dragPosition, setDragPosition] = useState<{x: number, y: number} | null>(null);
|
||||
|
||||
@@ -200,54 +200,54 @@ const PageEditor = ({
|
||||
const [thumbnailGenerationStarted, setThumbnailGenerationStarted] = useState(false);
|
||||
|
||||
// Thumbnail generation (opt-in for visual tools)
|
||||
const {
|
||||
const {
|
||||
generateThumbnails,
|
||||
addThumbnailToCache,
|
||||
getThumbnailFromCache,
|
||||
addThumbnailToCache,
|
||||
getThumbnailFromCache,
|
||||
stopGeneration,
|
||||
destroyThumbnails
|
||||
destroyThumbnails
|
||||
} = useThumbnailGeneration();
|
||||
|
||||
// Start thumbnail generation process (separate from document loading)
|
||||
const startThumbnailGeneration = useCallback(() => {
|
||||
console.log('🎬 PageEditor: startThumbnailGeneration called');
|
||||
console.log('🎬 Conditions - mergedPdfDocument:', !!mergedPdfDocument, 'activeFiles:', activeFiles.length, 'started:', thumbnailGenerationStarted);
|
||||
|
||||
|
||||
if (!mergedPdfDocument || activeFiles.length !== 1 || thumbnailGenerationStarted) {
|
||||
console.log('🎬 PageEditor: Skipping thumbnail generation due to conditions');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const file = activeFiles[0];
|
||||
const totalPages = mergedPdfDocument.totalPages;
|
||||
|
||||
|
||||
console.log('🎬 PageEditor: Starting thumbnail generation for', totalPages, 'pages');
|
||||
setThumbnailGenerationStarted(true);
|
||||
|
||||
|
||||
// Run everything asynchronously to avoid blocking the main thread
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// Load PDF array buffer for Web Workers
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
|
||||
// Generate page numbers for pages that don't have thumbnails yet
|
||||
const pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
.filter(pageNum => {
|
||||
const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum);
|
||||
return !page?.thumbnail; // Only generate for pages without thumbnails
|
||||
});
|
||||
|
||||
|
||||
console.log(`🎬 PageEditor: Generating thumbnails for ${pageNumbers.length} pages (out of ${totalPages} total):`, pageNumbers.slice(0, 10), pageNumbers.length > 10 ? '...' : '');
|
||||
|
||||
|
||||
// If no pages need thumbnails, we're done
|
||||
if (pageNumbers.length === 0) {
|
||||
console.log('🎬 PageEditor: All pages already have thumbnails, no generation needed');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Calculate quality scale based on file size
|
||||
const scale = activeFiles.length === 1 ? calculateScaleFromFileSize(activeFiles[0].size) : 0.2;
|
||||
|
||||
|
||||
// Start parallel thumbnail generation WITHOUT blocking the main thread
|
||||
const generationPromise = generateThumbnails(
|
||||
arrayBuffer,
|
||||
@@ -267,11 +267,11 @@ const PageEditor = ({
|
||||
// Check cache first, then send thumbnail
|
||||
const pageId = `${file.name}-page-${pageNumber}`;
|
||||
const cached = getThumbnailFromCache(pageId);
|
||||
|
||||
|
||||
if (!cached) {
|
||||
// Cache and send to component
|
||||
addThumbnailToCache(pageId, thumbnail);
|
||||
|
||||
|
||||
window.dispatchEvent(new CustomEvent('thumbnailReady', {
|
||||
detail: { pageNumber, thumbnail, pageId }
|
||||
}));
|
||||
@@ -292,7 +292,7 @@ const PageEditor = ({
|
||||
console.error('✗ PageEditor: Web Worker thumbnail generation failed:', error);
|
||||
setThumbnailGenerationStarted(false);
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start Web Worker thumbnail generation:', error);
|
||||
setThumbnailGenerationStarted(false);
|
||||
@@ -304,25 +304,25 @@ const PageEditor = ({
|
||||
useEffect(() => {
|
||||
console.log('🎬 PageEditor: Thumbnail generation effect triggered');
|
||||
console.log('🎬 Conditions - mergedPdfDocument:', !!mergedPdfDocument, 'started:', thumbnailGenerationStarted);
|
||||
|
||||
|
||||
if (mergedPdfDocument && !thumbnailGenerationStarted) {
|
||||
// Check if ALL pages already have thumbnails from processed files
|
||||
const totalPages = mergedPdfDocument.pages.length;
|
||||
const pagesWithThumbnails = mergedPdfDocument.pages.filter(page => page.thumbnail).length;
|
||||
const hasAllThumbnails = pagesWithThumbnails === totalPages;
|
||||
|
||||
|
||||
console.log('🎬 PageEditor: Thumbnail status:', {
|
||||
totalPages,
|
||||
pagesWithThumbnails,
|
||||
hasAllThumbnails,
|
||||
missingThumbnails: totalPages - pagesWithThumbnails
|
||||
});
|
||||
|
||||
|
||||
if (hasAllThumbnails) {
|
||||
console.log('🎬 PageEditor: Skipping generation - all thumbnails already exist');
|
||||
return; // Skip generation if ALL thumbnails already exist
|
||||
}
|
||||
|
||||
|
||||
console.log('🎬 PageEditor: Some thumbnails missing, proceeding with generation');
|
||||
// Small delay to let document render, then start thumbnail generation
|
||||
console.log('🎬 PageEditor: Scheduling thumbnail generation in 500ms');
|
||||
@@ -394,10 +394,10 @@ const PageEditor = ({
|
||||
|
||||
const togglePage = useCallback((pageNumber: number) => {
|
||||
console.log('🔄 Toggling page', pageNumber);
|
||||
|
||||
|
||||
// Check if currently selected and update accordingly
|
||||
const isCurrentlySelected = selectedPageNumbers.includes(pageNumber);
|
||||
|
||||
|
||||
if (isCurrentlySelected) {
|
||||
// Remove from selection
|
||||
console.log('🔄 Removing page', pageNumber);
|
||||
@@ -524,24 +524,24 @@ const PageEditor = ({
|
||||
// Update PDF document state with edit tracking
|
||||
const setPdfDocument = useCallback((updatedDoc: PDFDocument) => {
|
||||
console.log('setPdfDocument called - setting edited state');
|
||||
|
||||
|
||||
// Update local edit state for immediate visual feedback
|
||||
setEditedDocument(updatedDoc);
|
||||
setHasUnsavedChanges(true); // Use global state
|
||||
setHasUnsavedDraft(true); // Mark that we have unsaved draft changes
|
||||
|
||||
|
||||
// Auto-save to drafts (debounced) - only if we have new changes
|
||||
if (autoSaveTimer.current) {
|
||||
clearTimeout(autoSaveTimer.current);
|
||||
}
|
||||
|
||||
|
||||
autoSaveTimer.current = setTimeout(() => {
|
||||
if (hasUnsavedDraft) {
|
||||
saveDraftToIndexedDB(updatedDoc);
|
||||
setHasUnsavedDraft(false); // Mark draft as saved
|
||||
}
|
||||
}, 30000); // Auto-save after 30 seconds of inactivity
|
||||
|
||||
|
||||
return updatedDoc;
|
||||
}, [setHasUnsavedChanges, hasUnsavedDraft]);
|
||||
|
||||
@@ -554,7 +554,7 @@ const PageEditor = ({
|
||||
timestamp: Date.now(),
|
||||
originalFiles: activeFiles.map(f => f.name)
|
||||
};
|
||||
|
||||
|
||||
// Save to 'pdf-drafts' store in IndexedDB
|
||||
const request = indexedDB.open('stirling-pdf-drafts', 1);
|
||||
request.onupgradeneeded = () => {
|
||||
@@ -563,7 +563,7 @@ const PageEditor = ({
|
||||
db.createObjectStore('drafts');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
const transaction = db.transaction('drafts', 'readwrite');
|
||||
@@ -581,7 +581,7 @@ const PageEditor = ({
|
||||
try {
|
||||
const draftKey = `draft-${mergedPdfDocument?.id || 'merged'}`;
|
||||
const request = indexedDB.open('stirling-pdf-drafts', 1);
|
||||
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
const transaction = db.transaction('drafts', 'readwrite');
|
||||
@@ -596,12 +596,12 @@ const PageEditor = ({
|
||||
// Apply changes to create new processed file
|
||||
const applyChanges = useCallback(async () => {
|
||||
if (!editedDocument || !mergedPdfDocument) return;
|
||||
|
||||
|
||||
try {
|
||||
if (activeFiles.length === 1) {
|
||||
const file = activeFiles[0];
|
||||
const currentProcessedFile = processedFiles.get(file);
|
||||
|
||||
|
||||
if (currentProcessedFile) {
|
||||
const updatedProcessedFile = {
|
||||
...currentProcessedFile,
|
||||
@@ -614,14 +614,14 @@ const PageEditor = ({
|
||||
totalPages: editedDocument.pages.length,
|
||||
lastModified: Date.now()
|
||||
};
|
||||
|
||||
|
||||
updateProcessedFile(file, updatedProcessedFile);
|
||||
}
|
||||
} else if (activeFiles.length > 1) {
|
||||
setStatus('Apply changes for multiple files not yet supported');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Wait for the processed file update to complete before clearing edit state
|
||||
setTimeout(() => {
|
||||
setEditedDocument(null);
|
||||
@@ -630,7 +630,7 @@ const PageEditor = ({
|
||||
cleanupDraft();
|
||||
setStatus('Changes applied successfully');
|
||||
}, 100);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to apply changes:', error);
|
||||
setStatus('Failed to apply changes');
|
||||
@@ -653,7 +653,7 @@ const PageEditor = ({
|
||||
|
||||
// Skip animation for large documents (500+ pages) to improve performance
|
||||
const isLargeDocument = displayDocument.pages.length > 500;
|
||||
|
||||
|
||||
if (isLargeDocument) {
|
||||
// For large documents, just execute the command without animation
|
||||
if (pagesToMove.length > 1) {
|
||||
@@ -678,7 +678,7 @@ const PageEditor = ({
|
||||
|
||||
// Only capture positions for potentially affected pages
|
||||
const currentPositions = new Map<string, { x: number; y: number }>();
|
||||
|
||||
|
||||
affectedPageIds.forEach(pageId => {
|
||||
const element = document.querySelector(`[data-page-number="${pageId}"]`);
|
||||
if (element) {
|
||||
@@ -728,14 +728,14 @@ const PageEditor = ({
|
||||
|
||||
if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) {
|
||||
elementsToAnimate.push(element);
|
||||
|
||||
|
||||
// Apply initial transform
|
||||
element.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
||||
element.style.transition = 'none';
|
||||
|
||||
|
||||
// Force reflow
|
||||
element.offsetHeight;
|
||||
|
||||
|
||||
// Animate to final position
|
||||
element.style.transition = 'transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
||||
element.style.transform = 'translate(0px, 0px)';
|
||||
@@ -863,13 +863,13 @@ const PageEditor = ({
|
||||
if (!mergedPdfDocument) return;
|
||||
|
||||
// Convert page numbers to page IDs for export service
|
||||
const exportPageIds = selectedOnly
|
||||
const exportPageIds = selectedOnly
|
||||
? selectedPageNumbers.map(pageNum => {
|
||||
const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum);
|
||||
return page?.id || '';
|
||||
}).filter(id => id)
|
||||
: [];
|
||||
|
||||
|
||||
const preview = pdfExportService.getExportInfo(mergedPdfDocument, exportPageIds, selectedOnly);
|
||||
setExportPreview(preview);
|
||||
setShowExportModal(true);
|
||||
@@ -881,16 +881,16 @@ const PageEditor = ({
|
||||
setExportLoading(true);
|
||||
try {
|
||||
// Convert page numbers to page IDs for export service
|
||||
const exportPageIds = selectedOnly
|
||||
const exportPageIds = selectedOnly
|
||||
? selectedPageNumbers.map(pageNum => {
|
||||
const page = mergedPdfDocument.pages.find(p => p.pageNumber === pageNum);
|
||||
return page?.id || '';
|
||||
}).filter(id => id)
|
||||
: [];
|
||||
|
||||
|
||||
const errors = pdfExportService.validateExport(mergedPdfDocument, exportPageIds, selectedOnly);
|
||||
if (errors.length > 0) {
|
||||
setError(errors.join(', '));
|
||||
setStatus(errors.join(', '));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -921,7 +921,7 @@ const PageEditor = ({
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Export failed';
|
||||
setError(errorMessage);
|
||||
setStatus(errorMessage);
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
@@ -1009,26 +1009,26 @@ const PageEditor = ({
|
||||
// Check for existing drafts
|
||||
const checkForDrafts = useCallback(async () => {
|
||||
if (!mergedPdfDocument) return;
|
||||
|
||||
|
||||
try {
|
||||
const draftKey = `draft-${mergedPdfDocument.id || 'merged'}`;
|
||||
const request = indexedDB.open('stirling-pdf-drafts', 1);
|
||||
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains('drafts')) return;
|
||||
|
||||
|
||||
const transaction = db.transaction('drafts', 'readonly');
|
||||
const store = transaction.objectStore('drafts');
|
||||
const getRequest = store.get(draftKey);
|
||||
|
||||
|
||||
getRequest.onsuccess = () => {
|
||||
const draft = getRequest.result;
|
||||
if (draft && draft.timestamp) {
|
||||
// Check if draft is recent (within last 24 hours)
|
||||
const draftAge = Date.now() - draft.timestamp;
|
||||
const twentyFourHours = 24 * 60 * 60 * 1000;
|
||||
|
||||
|
||||
if (draftAge < twentyFourHours) {
|
||||
setFoundDraft(draft);
|
||||
setShowResumeModal(true);
|
||||
@@ -1066,12 +1066,12 @@ const PageEditor = ({
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
console.log('PageEditor unmounting - cleaning up resources');
|
||||
|
||||
|
||||
// Clear auto-save timer
|
||||
if (autoSaveTimer.current) {
|
||||
clearTimeout(autoSaveTimer.current);
|
||||
}
|
||||
|
||||
|
||||
// Clean up draft if component unmounts with unsaved changes
|
||||
if (hasUnsavedChanges) {
|
||||
cleanupDraft();
|
||||
@@ -1125,7 +1125,7 @@ const PageEditor = ({
|
||||
{showLoading && (
|
||||
<Box p="md" pt="xl">
|
||||
<SkeletonLoader type="controls" />
|
||||
|
||||
|
||||
{/* Progress indicator */}
|
||||
<Box mb="md" p="sm" style={{ backgroundColor: 'var(--mantine-color-blue-0)', borderRadius: 8 }}>
|
||||
<Group justify="space-between" mb="xs">
|
||||
@@ -1136,10 +1136,10 @@ const PageEditor = ({
|
||||
{Math.round(processingProgress || 0)}%
|
||||
</Text>
|
||||
</Group>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
borderRadius: '2px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
@@ -1151,7 +1151,7 @@ const PageEditor = ({
|
||||
}} />
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
|
||||
<SkeletonLoader type="pageGrid" count={8} />
|
||||
</Box>
|
||||
)}
|
||||
@@ -1165,10 +1165,10 @@ const PageEditor = ({
|
||||
<Text size="sm" fw={500}>Processing thumbnails...</Text>
|
||||
<Text size="sm" c="dimmed">{Math.round(processingProgress || 0)}%</Text>
|
||||
</Group>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '4px',
|
||||
backgroundColor: 'var(--mantine-color-gray-2)',
|
||||
borderRadius: '2px',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
@@ -1210,7 +1210,7 @@ const PageEditor = ({
|
||||
<Button onClick={deselectAll} variant="light">Deselect All</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
{/* Apply Changes Button */}
|
||||
{hasUnsavedChanges && (
|
||||
<Button
|
||||
@@ -1233,7 +1233,7 @@ const PageEditor = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<DragDropGrid
|
||||
items={displayedPages}
|
||||
selectedItems={selectedPageNumbers}
|
||||
@@ -1259,7 +1259,7 @@ const PageEditor = ({
|
||||
selectedPages={selectedPageNumbers}
|
||||
selectionMode={selectionMode}
|
||||
draggedPage={draggedPage}
|
||||
dropTarget={dropTarget}
|
||||
dropTarget={dropTarget === 'end' ? null : dropTarget}
|
||||
movingPage={movingPage}
|
||||
isAnimating={isAnimating}
|
||||
pageRefs={refs}
|
||||
@@ -1372,13 +1372,13 @@ const PageEditor = ({
|
||||
<Text>
|
||||
We found unsaved changes from a previous session. Would you like to resume where you left off?
|
||||
</Text>
|
||||
|
||||
|
||||
{foundDraft && (
|
||||
<Text size="sm" c="dimmed">
|
||||
Last saved: {new Date(foundDraft.timestamp).toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="light"
|
||||
@@ -1387,7 +1387,7 @@ const PageEditor = ({
|
||||
>
|
||||
Start Fresh
|
||||
</Button>
|
||||
|
||||
|
||||
<Button
|
||||
color="blue"
|
||||
onClick={resumeWork}
|
||||
|
||||
@@ -35,7 +35,7 @@ interface PageEditorControlsProps {
|
||||
|
||||
// Selection state
|
||||
selectionMode: boolean;
|
||||
selectedPages: string[];
|
||||
selectedPages: number[];
|
||||
}
|
||||
|
||||
const PageEditorControls = ({
|
||||
|
||||
@@ -7,9 +7,9 @@ import RotateRightIcon from '@mui/icons-material/RotateRight';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ContentCutIcon from '@mui/icons-material/ContentCut';
|
||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||
import { PDFPage, PDFDocument } from '../../../types/pageEditor';
|
||||
import { RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand } from '../../../commands/pageCommands';
|
||||
import { Command } from '../../../hooks/useUndoRedo';
|
||||
import { PDFPage, PDFDocument } from '../../types/pageEditor';
|
||||
import { RotatePagesCommand, DeletePagesCommand, ToggleSplitCommand } from '../../commands/pageCommands';
|
||||
import { Command } from '../../hooks/useUndoRedo';
|
||||
import styles from './PageEditor.module.css';
|
||||
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
|
||||
|
||||
@@ -29,7 +29,7 @@ interface PageThumbnailProps {
|
||||
selectedPages: number[];
|
||||
selectionMode: boolean;
|
||||
draggedPage: number | null;
|
||||
dropTarget: number | null;
|
||||
dropTarget: number | 'end' | null;
|
||||
movingPage: number | null;
|
||||
isAnimating: boolean;
|
||||
pageRefs: React.MutableRefObject<Map<string, HTMLDivElement>>;
|
||||
@@ -82,7 +82,7 @@ const PageThumbnail = React.memo(({
|
||||
}: PageThumbnailProps) => {
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||
const [isLoadingThumbnail, setIsLoadingThumbnail] = useState(false);
|
||||
|
||||
|
||||
// Update thumbnail URL when page prop changes
|
||||
useEffect(() => {
|
||||
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
|
||||
@@ -97,13 +97,13 @@ const PageThumbnail = React.memo(({
|
||||
console.log(`📸 PageThumbnail: Page ${page.pageNumber} already has thumbnail, skipping worker listener`);
|
||||
return; // Skip if we already have a thumbnail
|
||||
}
|
||||
|
||||
|
||||
console.log(`📸 PageThumbnail: Setting up worker listener for page ${page.pageNumber} (${page.id})`);
|
||||
|
||||
|
||||
const handleThumbnailReady = (event: CustomEvent) => {
|
||||
const { pageNumber, thumbnail, pageId } = event.detail;
|
||||
console.log(`📸 PageThumbnail: Received worker thumbnail for page ${pageNumber}, looking for page ${page.pageNumber} (${page.id})`);
|
||||
|
||||
|
||||
if (pageNumber === page.pageNumber && pageId === page.id) {
|
||||
console.log(`✓ PageThumbnail: Thumbnail matched for page ${page.pageNumber}, setting URL`);
|
||||
setThumbnailUrl(thumbnail);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Card, Stack, Text, Group, Badge, Button, Box, Image, ThemeIcon, ActionIcon, Tooltip } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
||||
@@ -9,7 +9,6 @@ import EditIcon from "@mui/icons-material/Edit";
|
||||
import { FileWithUrl } from "../../types/file";
|
||||
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
||||
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
||||
import { fileStorage } from "../../services/fileStorage";
|
||||
|
||||
interface FileCardProps {
|
||||
file: FileWithUrl;
|
||||
|
||||
@@ -80,7 +80,7 @@ const FileGrid = ({
|
||||
{showSearch && (
|
||||
<TextInput
|
||||
placeholder={t("fileManager.searchFiles", "Search files...")}
|
||||
leftSection={<SearchIcon size={16} />}
|
||||
leftSection={<SearchIcon fontSize="small" />}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.currentTarget.value)}
|
||||
style={{ flexGrow: 1, maxWidth: 300, minWidth: 200 }}
|
||||
@@ -96,7 +96,7 @@ const FileGrid = ({
|
||||
]}
|
||||
value={sortBy}
|
||||
onChange={(value) => setSortBy(value as SortOption)}
|
||||
leftSection={<SortIcon size={16} />}
|
||||
leftSection={<SortIcon fontSize="small" />}
|
||||
style={{ minWidth: 150 }}
|
||||
/>
|
||||
)}
|
||||
@@ -130,7 +130,7 @@ const FileGrid = ({
|
||||
<FileCard
|
||||
key={fileId + idx}
|
||||
file={file}
|
||||
onRemove={onRemove ? () => onRemove(originalIdx) : undefined}
|
||||
onRemove={onRemove ? () => onRemove(originalIdx) : () => {}}
|
||||
onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(file) : undefined}
|
||||
onView={onView && supported ? () => onView(file) : undefined}
|
||||
onEdit={onEdit && supported ? () => onEdit(file) : undefined}
|
||||
|
||||
@@ -64,19 +64,19 @@ const TopControls = ({
|
||||
}: TopControlsProps) => {
|
||||
const { themeMode, isRainbowMode, isToggleDisabled, toggleTheme } = useRainbowThemeContext();
|
||||
const [switchingTo, setSwitchingTo] = useState<string | null>(null);
|
||||
|
||||
|
||||
const isToolSelected = selectedToolKey !== null;
|
||||
|
||||
const handleViewChange = useCallback((view: string) => {
|
||||
// Show immediate feedback
|
||||
setSwitchingTo(view);
|
||||
|
||||
|
||||
// Defer the heavy view change to next frame so spinner can render
|
||||
requestAnimationFrame(() => {
|
||||
// Give the spinner one more frame to show
|
||||
requestAnimationFrame(() => {
|
||||
setCurrentView(view);
|
||||
|
||||
|
||||
// Clear the loading state after view change completes
|
||||
setTimeout(() => setSwitchingTo(null), 300);
|
||||
});
|
||||
|
||||
@@ -16,24 +16,24 @@ export default function ToolPanel() {
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { toolPanelRef } = sidebarRefs;
|
||||
|
||||
|
||||
|
||||
// Use context-based hooks to eliminate prop drilling
|
||||
const {
|
||||
leftPanelView,
|
||||
isPanelVisible,
|
||||
searchQuery,
|
||||
const {
|
||||
leftPanelView,
|
||||
isPanelVisible,
|
||||
searchQuery,
|
||||
filteredTools,
|
||||
setSearchQuery,
|
||||
handleBackToTools
|
||||
} = useToolPanelState();
|
||||
|
||||
|
||||
const { selectedToolKey, handleToolSelect } = useToolSelection();
|
||||
const { setPreviewFile } = useWorkbenchState();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={toolPanelRef}
|
||||
data-sidebar="tool-panel"
|
||||
data-sidebar="tool-panel"
|
||||
className={`h-screen flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
isRainbowMode ? rainbowStyles.rainbowPaper : ''
|
||||
}`}
|
||||
@@ -77,7 +77,7 @@ export default function ToolPanel() {
|
||||
{/* Tool content */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<ToolRenderer
|
||||
selectedToolKey={selectedToolKey}
|
||||
selectedToolKey={selectedToolKey || ''}
|
||||
onPreviewFile={setPreviewFile}
|
||||
/>
|
||||
</div>
|
||||
@@ -86,4 +86,4 @@ export default function ToolPanel() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ const ConvertFromImageSettings = ({
|
||||
})}
|
||||
data={[
|
||||
{ value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") },
|
||||
{ value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") },
|
||||
{ value: COLOR_TYPES.GRAYSCALE, label: t("convert.grayscale", "Grayscale") },
|
||||
{ value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") },
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
|
||||
<Select
|
||||
data-testid="fit-option-select"
|
||||
label={t("convert.fitOption", "Fit Option")}
|
||||
@@ -51,7 +51,7 @@ const ConvertFromImageSettings = ({
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
|
||||
<Switch
|
||||
data-testid="auto-rotate-switch"
|
||||
label={t("convert.autoRotate", "Auto Rotate")}
|
||||
@@ -63,7 +63,7 @@ const ConvertFromImageSettings = ({
|
||||
})}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
|
||||
<Switch
|
||||
data-testid="combine-images-switch"
|
||||
label={t("convert.combineImages", "Combine Images")}
|
||||
@@ -79,4 +79,4 @@ const ConvertFromImageSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertFromImageSettings;
|
||||
export default ConvertFromImageSettings;
|
||||
|
||||
@@ -31,7 +31,6 @@ const ConvertFromWebSettings = ({
|
||||
min={0.1}
|
||||
max={3.0}
|
||||
step={0.1}
|
||||
precision={1}
|
||||
disabled={disabled}
|
||||
data-testid="zoom-level-input"
|
||||
/>
|
||||
|
||||
@@ -31,7 +31,7 @@ const ConvertToImageSettings = ({
|
||||
})}
|
||||
data={[
|
||||
{ value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") },
|
||||
{ value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") },
|
||||
{ value: COLOR_TYPES.GRAYSCALE, label: t("convert.grayscale", "Grayscale") },
|
||||
{ value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") },
|
||||
]}
|
||||
disabled={disabled}
|
||||
@@ -68,4 +68,4 @@ const ConvertToImageSettings = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ConvertToImageSettings;
|
||||
export default ConvertToImageSettings;
|
||||
|
||||
@@ -30,7 +30,7 @@ const ConvertToPdfaSettings = ({
|
||||
<Text size="sm" fw={500}>{t("convert.pdfaOptions", "PDF/A Options")}:</Text>
|
||||
|
||||
{hasDigitalSignatures && (
|
||||
<Alert color="yellow" size="sm">
|
||||
<Alert color="yellow">
|
||||
<Text size="sm">
|
||||
{t("convert.pdfaDigitalSignatureWarning", "The PDF contains a digital signature. This will be removed in the next step.")}
|
||||
</Text>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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';
|
||||
import { isSplitMode, SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants';
|
||||
|
||||
export interface SplitParameters {
|
||||
mode: SplitMode | '';
|
||||
@@ -123,7 +123,7 @@ const SplitSettings = ({
|
||||
label="Choose split method"
|
||||
placeholder="Select how to split the PDF"
|
||||
value={parameters.mode}
|
||||
onChange={(v) => v && onParameterChange('mode', v)}
|
||||
onChange={(v) => isSplitMode(v) && onParameterChange('mode', v)}
|
||||
disabled={disabled}
|
||||
data={[
|
||||
{ value: SPLIT_MODES.BY_PAGES, label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" },
|
||||
|
||||
@@ -137,7 +137,7 @@ export interface ViewerProps {
|
||||
sidebarsVisible: boolean;
|
||||
setSidebarsVisible: (v: boolean) => void;
|
||||
onClose?: () => void;
|
||||
previewFile?: File; // For preview mode - bypasses context
|
||||
previewFile: File | null; // For preview mode - bypasses context
|
||||
}
|
||||
|
||||
const Viewer = ({
|
||||
@@ -148,18 +148,13 @@ const Viewer = ({
|
||||
}: ViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useMantineTheme();
|
||||
|
||||
|
||||
// Get current file from FileContext
|
||||
const { getCurrentFile, getCurrentProcessedFile, clearAllFiles, addFiles, activeFiles } = useFileContext();
|
||||
const currentFile = getCurrentFile();
|
||||
const processedFile = getCurrentProcessedFile();
|
||||
|
||||
// Convert File to FileWithUrl format for viewer
|
||||
const pdfFile = useFileWithUrl(currentFile);
|
||||
|
||||
|
||||
// Tab management for multiple files
|
||||
const [activeTab, setActiveTab] = useState<string>("0");
|
||||
|
||||
|
||||
// Reset PDF state when switching tabs
|
||||
const handleTabChange = (newTab: string) => {
|
||||
setActiveTab(newTab);
|
||||
@@ -183,7 +178,7 @@ const Viewer = ({
|
||||
const file2WithUrl = useFileWithUrl(activeFiles[2]);
|
||||
const file3WithUrl = useFileWithUrl(activeFiles[3]);
|
||||
const file4WithUrl = useFileWithUrl(activeFiles[4]);
|
||||
|
||||
|
||||
const filesWithUrls = React.useMemo(() => {
|
||||
return [file0WithUrl, file1WithUrl, file2WithUrl, file3WithUrl, file4WithUrl]
|
||||
.slice(0, activeFiles.length)
|
||||
@@ -197,11 +192,11 @@ const Viewer = ({
|
||||
if (!(previewFile instanceof File)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (previewFile.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return { file: previewFile, url: null };
|
||||
} else {
|
||||
// Use the file from the active tab
|
||||
@@ -262,12 +257,12 @@ const Viewer = ({
|
||||
// Progressive preloading function
|
||||
const startProgressivePreload = async () => {
|
||||
if (!pdfDocRef.current || preloadingRef.current || numPages === 0) return;
|
||||
|
||||
|
||||
preloadingRef.current = true;
|
||||
|
||||
|
||||
// Start with first few pages for immediate viewing
|
||||
const priorityPages = [0, 1, 2, 3, 4]; // First 5 pages
|
||||
|
||||
|
||||
// Render priority pages first
|
||||
for (const pageIndex of priorityPages) {
|
||||
if (pageIndex < numPages && !pageImages[pageIndex]) {
|
||||
@@ -276,7 +271,7 @@ const Viewer = ({
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Then render remaining pages in background
|
||||
for (let pageIndex = 5; pageIndex < numPages; pageIndex++) {
|
||||
if (!pageImages[pageIndex]) {
|
||||
@@ -285,7 +280,7 @@ const Viewer = ({
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
preloadingRef.current = false;
|
||||
};
|
||||
|
||||
@@ -300,15 +295,15 @@ const Viewer = ({
|
||||
const scrollToPage = (pageNumber: number) => {
|
||||
const el = pageRefs.current[pageNumber - 1];
|
||||
const scrollArea = scrollAreaRef.current;
|
||||
|
||||
|
||||
if (el && scrollArea) {
|
||||
const scrollAreaRect = scrollArea.getBoundingClientRect();
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const currentScrollTop = scrollArea.scrollTop;
|
||||
|
||||
|
||||
// Position page near top of viewport with some padding
|
||||
const targetScrollTop = currentScrollTop + (elRect.top - scrollAreaRect.top) - 20;
|
||||
|
||||
|
||||
scrollArea.scrollTo({
|
||||
top: targetScrollTop,
|
||||
behavior: "smooth"
|
||||
@@ -364,7 +359,7 @@ const Viewer = ({
|
||||
setLoading(true);
|
||||
try {
|
||||
let pdfData;
|
||||
|
||||
|
||||
// For preview files, use ArrayBuffer directly to avoid blob URL issues
|
||||
if (previewFile && effectiveFile.file === previewFile) {
|
||||
const arrayBuffer = await previewFile.arrayBuffer();
|
||||
@@ -446,7 +441,7 @@ const Viewer = ({
|
||||
<CloseIcon />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
|
||||
{!effectiveFile ? (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Text c="red">Error: No file provided to viewer</Text>
|
||||
@@ -455,8 +450,8 @@ const Viewer = ({
|
||||
<>
|
||||
{/* Tabs for multiple files */}
|
||||
{activeFiles.length > 1 && !previewFile && (
|
||||
<Box
|
||||
style={{
|
||||
<Box
|
||||
style={{
|
||||
borderBottom: '1px solid var(--mantine-color-gray-3)',
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
position: 'relative',
|
||||
@@ -475,7 +470,7 @@ const Viewer = ({
|
||||
</Tabs>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{loading ? (
|
||||
<div style={{ flex: 1, padding: '1rem' }}>
|
||||
<SkeletonLoader type="viewer" />
|
||||
|
||||
Reference in New Issue
Block a user