mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
feat: Implement file pinning functionality and enhance file context management
This commit is contained in:
parent
9552772587
commit
eed06859b3
@ -1,6 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||
import { getFileSize, getFileDate } from '../../utils/fileUtils';
|
||||
import { FileMetadata } from '../../types/file';
|
||||
|
||||
@ -8,8 +10,11 @@ interface FileListItemProps {
|
||||
file: FileMetadata;
|
||||
isSelected: boolean;
|
||||
isSupported: boolean;
|
||||
isPinned?: boolean;
|
||||
onSelect: () => void;
|
||||
onRemove: () => void;
|
||||
onPin?: () => void;
|
||||
onUnpin?: () => void;
|
||||
onDoubleClick?: () => void;
|
||||
isLast?: boolean;
|
||||
}
|
||||
@ -18,8 +23,11 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
file,
|
||||
isSelected,
|
||||
isSupported,
|
||||
isPinned = false,
|
||||
onSelect,
|
||||
onRemove,
|
||||
onPin,
|
||||
onUnpin,
|
||||
onDoubleClick
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
@ -59,6 +67,36 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
<Text size="sm" fw={500} truncate>{file.name}</Text>
|
||||
<Text size="xs" c="dimmed">{getFileSize(file)} • {getFileDate(file)}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Pin button - always visible for pinned files, fades in/out on hover for unpinned */}
|
||||
{(onPin || onUnpin) && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
c={isPinned ? "blue" : "dimmed"}
|
||||
size="md"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isPinned) {
|
||||
onUnpin?.();
|
||||
} else {
|
||||
onPin?.();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
opacity: isPinned ? 1 : (isHovered ? 1 : 0),
|
||||
transform: isPinned ? 'scale(1)' : (isHovered ? 'scale(1)' : 'scale(0.8)'),
|
||||
transition: 'opacity 0.3s ease, transform 0.3s ease',
|
||||
pointerEvents: isPinned ? 'auto' : (isHovered ? 'auto' : 'none')
|
||||
}}
|
||||
>
|
||||
{isPinned ? (
|
||||
<PushPinIcon style={{ fontSize: 18 }} />
|
||||
) : (
|
||||
<PushPinOutlinedIcon style={{ fontSize: 18 }} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
{/* Delete button - fades in/out on hover */}
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mantine/core';
|
||||
import { FileWithUrl } from '../../types/file';
|
||||
import { FileWithUrl, FileMetadata } from '../../types/file';
|
||||
import DocumentThumbnail from './filePreview/DocumentThumbnail';
|
||||
import DocumentStack from './filePreview/DocumentStack';
|
||||
import HoverOverlay from './filePreview/HoverOverlay';
|
||||
@ -8,7 +8,7 @@ import NavigationArrows from './filePreview/NavigationArrows';
|
||||
|
||||
export interface FilePreviewProps {
|
||||
// Core file data
|
||||
file: File | FileWithUrl | null;
|
||||
file: File | FileWithUrl | FileMetadata | null;
|
||||
thumbnail?: string | null;
|
||||
|
||||
// Optional features
|
||||
@ -21,7 +21,7 @@ export interface FilePreviewProps {
|
||||
isAnimating?: boolean;
|
||||
|
||||
// Event handlers
|
||||
onFileClick?: (file: File | FileWithUrl | null) => void;
|
||||
onFileClick?: (file: File | FileWithUrl | FileMetadata | null) => void;
|
||||
onPrevious?: () => void;
|
||||
onNext?: () => void;
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Box, Center, Image } from '@mantine/core';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import { FileWithUrl } from '../../../types/file';
|
||||
import { FileWithUrl, FileMetadata } from '../../../types/file';
|
||||
|
||||
export interface DocumentThumbnailProps {
|
||||
file: File | FileWithUrl | null;
|
||||
file: File | FileWithUrl | FileMetadata | null;
|
||||
thumbnail?: string | null;
|
||||
style?: React.CSSProperties;
|
||||
onClick?: () => void;
|
||||
|
@ -88,6 +88,36 @@ export function FileContextProvider({
|
||||
return consumeFiles(inputFileIds, outputFiles, stateRef, filesRef, dispatch);
|
||||
}, []);
|
||||
|
||||
// Helper to find FileId from File object
|
||||
const findFileId = useCallback((file: File): FileId | undefined => {
|
||||
return Object.keys(stateRef.current.files.byId).find(id => {
|
||||
const storedFile = filesRef.current.get(id);
|
||||
return storedFile &&
|
||||
storedFile.name === file.name &&
|
||||
storedFile.size === file.size &&
|
||||
storedFile.lastModified === file.lastModified;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// File-to-ID wrapper functions for pinning
|
||||
const pinFileWrapper = useCallback((file: File) => {
|
||||
const fileId = findFileId(file);
|
||||
if (fileId) {
|
||||
baseActions.pinFile(fileId);
|
||||
} else {
|
||||
console.warn('File not found for pinning:', file.name);
|
||||
}
|
||||
}, [baseActions, findFileId]);
|
||||
|
||||
const unpinFileWrapper = useCallback((file: File) => {
|
||||
const fileId = findFileId(file);
|
||||
if (fileId) {
|
||||
baseActions.unpinFile(fileId);
|
||||
} else {
|
||||
console.warn('File not found for unpinning:', file.name);
|
||||
}
|
||||
}, [baseActions, findFileId]);
|
||||
|
||||
// Complete actions object
|
||||
const actions = useMemo<FileContextActions>(() => ({
|
||||
...baseActions,
|
||||
@ -103,7 +133,9 @@ export function FileContextProvider({
|
||||
filesRef.current.clear();
|
||||
dispatch({ type: 'RESET_CONTEXT' });
|
||||
},
|
||||
// Pinned files functionality - isFilePinned available in selectors
|
||||
// Pinned files functionality with File object wrappers
|
||||
pinFile: pinFileWrapper,
|
||||
unpinFile: unpinFileWrapper,
|
||||
consumeFiles: consumeFilesWrapper,
|
||||
setHasUnsavedChanges,
|
||||
trackBlobUrl: lifecycleManager.trackBlobUrl,
|
||||
@ -118,7 +150,9 @@ export function FileContextProvider({
|
||||
addStoredFiles,
|
||||
lifecycleManager,
|
||||
setHasUnsavedChanges,
|
||||
consumeFilesWrapper
|
||||
consumeFilesWrapper,
|
||||
pinFileWrapper,
|
||||
unpinFileWrapper
|
||||
]);
|
||||
|
||||
// Split context values to minimize re-renders
|
||||
|
@ -15,6 +15,7 @@ export const initialFileContextState: FileContextState = {
|
||||
ids: [],
|
||||
byId: {}
|
||||
},
|
||||
pinnedFiles: new Set(),
|
||||
ui: {
|
||||
selectedFileIds: [],
|
||||
selectedPageNumbers: [],
|
||||
@ -153,6 +154,66 @@ export function fileContextReducer(state: FileContextState, action: FileContextA
|
||||
};
|
||||
}
|
||||
|
||||
case 'PIN_FILE': {
|
||||
const { fileId } = action.payload;
|
||||
const newPinnedFiles = new Set(state.pinnedFiles);
|
||||
newPinnedFiles.add(fileId);
|
||||
|
||||
return {
|
||||
...state,
|
||||
pinnedFiles: newPinnedFiles
|
||||
};
|
||||
}
|
||||
|
||||
case 'UNPIN_FILE': {
|
||||
const { fileId } = action.payload;
|
||||
const newPinnedFiles = new Set(state.pinnedFiles);
|
||||
newPinnedFiles.delete(fileId);
|
||||
|
||||
return {
|
||||
...state,
|
||||
pinnedFiles: newPinnedFiles
|
||||
};
|
||||
}
|
||||
|
||||
case 'CONSUME_FILES': {
|
||||
const { inputFileIds, outputFileRecords } = action.payload;
|
||||
|
||||
// Only remove unpinned input files
|
||||
const unpinnedInputIds = inputFileIds.filter(id => !state.pinnedFiles.has(id));
|
||||
const remainingIds = state.files.ids.filter(id => !unpinnedInputIds.includes(id));
|
||||
|
||||
// Remove unpinned files from state
|
||||
const newById = { ...state.files.byId };
|
||||
unpinnedInputIds.forEach(id => {
|
||||
delete newById[id];
|
||||
});
|
||||
|
||||
// Add output files
|
||||
const outputIds: FileId[] = [];
|
||||
outputFileRecords.forEach(record => {
|
||||
if (!newById[record.id]) {
|
||||
outputIds.push(record.id);
|
||||
newById[record.id] = record;
|
||||
}
|
||||
});
|
||||
|
||||
// Clear selections that reference removed files
|
||||
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !unpinnedInputIds.includes(id));
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: {
|
||||
ids: [...remainingIds, ...outputIds],
|
||||
byId: newById
|
||||
},
|
||||
ui: {
|
||||
...state.ui,
|
||||
selectedFileIds: validSelectedFileIds
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
case 'RESET_CONTEXT': {
|
||||
// Reset UI state to clean slate (resource cleanup handled by lifecycle manager)
|
||||
return { ...initialFileContextState };
|
||||
|
@ -212,6 +212,62 @@ export async function addFiles(
|
||||
return addedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume files helper - replace unpinned input files with output files
|
||||
*/
|
||||
export async function consumeFiles(
|
||||
inputFileIds: FileId[],
|
||||
outputFiles: File[],
|
||||
stateRef: React.MutableRefObject<FileContextState>,
|
||||
filesRef: React.MutableRefObject<Map<FileId, File>>,
|
||||
dispatch: React.Dispatch<FileContextAction>
|
||||
): Promise<void> {
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Processing ${inputFileIds.length} input files, ${outputFiles.length} output files`);
|
||||
|
||||
// Process output files through the 'processed' path to generate thumbnails
|
||||
const outputFileRecords = await Promise.all(
|
||||
outputFiles.map(async (file) => {
|
||||
const fileId = createFileId();
|
||||
filesRef.current.set(fileId, file);
|
||||
|
||||
// Generate thumbnail and page count for output file
|
||||
let thumbnail: string | undefined;
|
||||
let pageCount: number = 1;
|
||||
|
||||
try {
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Generating thumbnail for output file ${file.name}`);
|
||||
const result = await generateThumbnailWithMetadata(file);
|
||||
thumbnail = result.thumbnail;
|
||||
pageCount = result.pageCount;
|
||||
} catch (error) {
|
||||
if (DEBUG) console.warn(`📄 consumeFiles: Failed to generate thumbnail for output file ${file.name}:`, error);
|
||||
}
|
||||
|
||||
const record = toFileRecord(file, fileId);
|
||||
if (thumbnail) {
|
||||
record.thumbnailUrl = thumbnail;
|
||||
}
|
||||
|
||||
if (pageCount > 0) {
|
||||
record.processedFile = createProcessedFile(pageCount, thumbnail);
|
||||
}
|
||||
|
||||
return record;
|
||||
})
|
||||
);
|
||||
|
||||
// Dispatch the consume action
|
||||
dispatch({
|
||||
type: 'CONSUME_FILES',
|
||||
payload: {
|
||||
inputFileIds,
|
||||
outputFileRecords
|
||||
}
|
||||
});
|
||||
|
||||
if (DEBUG) console.log(`📄 consumeFiles: Successfully consumed files - removed ${inputFileIds.length} inputs, added ${outputFileRecords.length} outputs`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action factory functions
|
||||
*/
|
||||
@ -221,5 +277,7 @@ export const createFileActions = (dispatch: React.Dispatch<FileContextAction>) =
|
||||
clearSelections: () => dispatch({ type: 'CLEAR_SELECTIONS' }),
|
||||
setProcessing: (isProcessing: boolean, progress = 0) => dispatch({ type: 'SET_PROCESSING', payload: { isProcessing, progress } }),
|
||||
setHasUnsavedChanges: (hasChanges: boolean) => dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } }),
|
||||
pinFile: (fileId: FileId) => dispatch({ type: 'PIN_FILE', payload: { fileId } }),
|
||||
unpinFile: (fileId: FileId) => dispatch({ type: 'UNPIN_FILE', payload: { fileId } }),
|
||||
resetContext: () => dispatch({ type: 'RESET_CONTEXT' })
|
||||
});
|
@ -144,14 +144,32 @@ export function useSelectedFiles(): { files: File[]; records: FileRecord[]; file
|
||||
* Used by tools for core file context functionality
|
||||
*/
|
||||
export function useFileContext() {
|
||||
const { state, selectors } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
|
||||
return useMemo(() => ({
|
||||
// Lifecycle management
|
||||
trackBlobUrl: actions.trackBlobUrl,
|
||||
trackPdfDocument: actions.trackPdfDocument,
|
||||
scheduleCleanup: actions.scheduleCleanup,
|
||||
setUnsavedChanges: actions.setHasUnsavedChanges
|
||||
}), [actions]);
|
||||
setUnsavedChanges: actions.setHasUnsavedChanges,
|
||||
|
||||
// File management
|
||||
addFiles: actions.addFiles,
|
||||
consumeFiles: actions.consumeFiles,
|
||||
recordOperation: (fileId: string, operation: any) => {}, // TODO: Implement operation tracking
|
||||
markOperationApplied: (fileId: string, operationId: string) => {}, // TODO: Implement operation tracking
|
||||
markOperationFailed: (fileId: string, operationId: string, error: string) => {}, // TODO: Implement operation tracking
|
||||
|
||||
// Pinned files
|
||||
pinnedFiles: state.pinnedFiles,
|
||||
pinFile: actions.pinFile,
|
||||
unpinFile: actions.unpinFile,
|
||||
isFilePinned: selectors.isFilePinned,
|
||||
|
||||
// Active files
|
||||
activeFiles: selectors.getFiles()
|
||||
}), [state, selectors, actions]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,6 +45,35 @@ export function createFileSelectors(
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
// Pinned files selectors
|
||||
getPinnedFileIds: () => {
|
||||
return Array.from(stateRef.current.pinnedFiles);
|
||||
},
|
||||
|
||||
getPinnedFiles: () => {
|
||||
return Array.from(stateRef.current.pinnedFiles)
|
||||
.map(id => filesRef.current.get(id))
|
||||
.filter(Boolean) as File[];
|
||||
},
|
||||
|
||||
getPinnedFileRecords: () => {
|
||||
return Array.from(stateRef.current.pinnedFiles)
|
||||
.map(id => stateRef.current.files.byId[id])
|
||||
.filter(Boolean);
|
||||
},
|
||||
|
||||
isFilePinned: (file: File) => {
|
||||
// Find FileId by matching File object properties
|
||||
const fileId = Object.keys(stateRef.current.files.byId).find(id => {
|
||||
const storedFile = filesRef.current.get(id);
|
||||
return storedFile &&
|
||||
storedFile.name === file.name &&
|
||||
storedFile.size === file.size &&
|
||||
storedFile.lastModified === file.lastModified;
|
||||
});
|
||||
return fileId ? stateRef.current.pinnedFiles.has(fileId) : false;
|
||||
},
|
||||
|
||||
// Stable signature for effects - prevents unnecessary re-renders
|
||||
getFilesSignature: () => {
|
||||
return stateRef.current.files.ids
|
||||
|
@ -104,7 +104,7 @@ export const useToolOperation = <TParams = void>(
|
||||
config: ToolOperationConfig<TParams>
|
||||
): ToolOperationHook<TParams> => {
|
||||
const { t } = useTranslation();
|
||||
const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles } = useFileContext();
|
||||
const { recordOperation, markOperationApplied, markOperationFailed, addFiles } = useFileContext();
|
||||
|
||||
// Composed hooks
|
||||
const { state, actions } = useToolState();
|
||||
@ -198,8 +198,8 @@ export const useToolOperation = <TParams = void>(
|
||||
actions.setThumbnails(thumbnails);
|
||||
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
||||
|
||||
// Consume input files and add output files (will replace unpinned inputs)
|
||||
await consumeFiles(validFiles, processedFiles);
|
||||
// Add processed files to the file context
|
||||
await addFiles(processedFiles);
|
||||
|
||||
markOperationApplied(fileId, operationId);
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ import { BaseToolProps } from "../types/tool";
|
||||
const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
// const setCurrentMode = (mode) => console.log('Navigate to:', mode); // TODO: Hook up to URL routing
|
||||
|
||||
const [collapsedPermissions, setCollapsedPermissions] = useState(true);
|
||||
|
||||
@ -50,7 +49,6 @@ const AddPassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "addPassword");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
@ -25,7 +24,6 @@ import { BaseToolProps } from "../types/tool";
|
||||
|
||||
const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setCurrentMode } = useFileContext();
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
|
||||
const [collapsedType, setCollapsedType] = useState(false);
|
||||
@ -71,13 +69,11 @@ const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =>
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "watermark");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
watermarkOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode("watermark");
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEndpointEnabled } from "../hooks/useEndpointConfig";
|
||||
import { useFileContext } from "../contexts/FileContext";
|
||||
import { useToolFileSelection } from "../contexts/FileSelectionContext";
|
||||
import { useToolFileSelection } from "../contexts/FileContext";
|
||||
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
|
||||
@ -15,7 +14,6 @@ import { BaseToolProps } from "../types/tool";
|
||||
|
||||
const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { setCurrentMode } = useFileContext();
|
||||
const { selectedFiles } = useToolFileSelection();
|
||||
|
||||
const removePasswordParams = useRemovePasswordParameters();
|
||||
@ -46,13 +44,11 @@ const RemovePassword = ({ onPreviewFile, onComplete, onError }: BaseToolProps) =
|
||||
const handleThumbnailClick = (file: File) => {
|
||||
onPreviewFile?.(file);
|
||||
sessionStorage.setItem("previousMode", "removePassword");
|
||||
setCurrentMode("viewer");
|
||||
};
|
||||
|
||||
const handleSettingsReset = () => {
|
||||
removePasswordOperation.resetResults();
|
||||
onPreviewFile?.(null);
|
||||
setCurrentMode("removePassword");
|
||||
};
|
||||
|
||||
const hasFiles = selectedFiles.length > 0;
|
||||
|
@ -16,15 +16,15 @@ interface ColorScheme {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate thumbnail scale based on file size
|
||||
* Calculate thumbnail scale based on file size (modern 2024 scaling)
|
||||
*/
|
||||
export function calculateScaleFromFileSize(fileSize: number): number {
|
||||
const MB = 1024 * 1024;
|
||||
if (fileSize < 1 * MB) return 0.6;
|
||||
if (fileSize < 5 * MB) return 0.4;
|
||||
if (fileSize < 15 * MB) return 0.3;
|
||||
if (fileSize < 30 * MB) return 0.2;
|
||||
return 0.15;
|
||||
if (fileSize < 10 * MB) return 1.0; // Full quality for small files
|
||||
if (fileSize < 50 * MB) return 0.8; // High quality for common file sizes
|
||||
if (fileSize < 200 * MB) return 0.6; // Good quality for typical large files
|
||||
if (fileSize < 500 * MB) return 0.4; // Readable quality for large but manageable files
|
||||
return 0.3; // Still usable quality, not tiny
|
||||
}
|
||||
|
||||
/**
|
||||
@ -341,9 +341,21 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
|
||||
// Handle PDF files
|
||||
if (file.type.startsWith('application/pdf')) {
|
||||
const scale = calculateScaleFromFileSize(file.size);
|
||||
|
||||
// Only read first 2MB for thumbnail generation to save memory
|
||||
const chunkSize = 2 * 1024 * 1024; // 2MB
|
||||
const chunk = file.slice(0, Math.min(chunkSize, file.size));
|
||||
const arrayBuffer = await chunk.arrayBuffer();
|
||||
|
||||
try {
|
||||
return await generatePdfThumbnail(file, scale);
|
||||
return await generatePDFThumbnail(arrayBuffer, file, scale);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'InvalidPDFException') {
|
||||
console.warn(`PDF structure issue for ${file.name} - using fallback thumbnail`);
|
||||
// Try with full file instead of chunk
|
||||
const fullArrayBuffer = await file.arrayBuffer();
|
||||
return await generatePDFThumbnail(fullArrayBuffer, file, scale);
|
||||
}
|
||||
return generatePlaceholderThumbnail(file);
|
||||
}
|
||||
}
|
||||
@ -369,15 +381,6 @@ export async function generateThumbnailWithMetadata(file: File): Promise<Thumbna
|
||||
}
|
||||
|
||||
const scale = calculateScaleFromFileSize(file.size);
|
||||
console.log(`Using scale ${scale} for ${file.name} (${(file.size / 1024 / 1024).toFixed(1)}MB)`);
|
||||
|
||||
// Only read first 2MB for thumbnail generation to save memory
|
||||
const chunkSize = 2 * 1024 * 1024; // 2MB
|
||||
const chunk = file.slice(0, Math.min(chunkSize, file.size));
|
||||
const arrayBuffer = await chunk.arrayBuffer();
|
||||
|
||||
try {
|
||||
return await generatePDFThumbnail(arrayBuffer, file, scale);
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
@ -403,18 +406,13 @@ export async function generateThumbnailWithMetadata(file: File): Promise<Thumbna
|
||||
return { thumbnail, pageCount };
|
||||
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'InvalidPDFException') {
|
||||
console.warn(`PDF structure issue for ${file.name} - using fallback thumbnail`);
|
||||
// Return a placeholder or try with full file instead of chunk
|
||||
const fullArrayBuffer = await file.arrayBuffer();
|
||||
return await generatePDFThumbnail(fullArrayBuffer, file, scale);
|
||||
} else {
|
||||
console.warn('Unknown error thrown. Failed to generate thumbnail for', file.name, error);
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
throw error; // Re-throw non-Error exceptions
|
||||
if (error instanceof Error && error.name === "PasswordException") {
|
||||
// Handle encrypted PDFs
|
||||
const thumbnail = generateEncryptedPDFThumbnail(file);
|
||||
return { thumbnail, pageCount: 1 };
|
||||
}
|
||||
|
||||
const thumbnail = generatePlaceholderThumbnail(file);
|
||||
return { thumbnail, pageCount: 1 };
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user