diff --git a/frontend/src/components/FileCard.standalone.tsx b/frontend/src/components/FileCard.standalone.tsx deleted file mode 100644 index 4d140689b..000000000 --- a/frontend/src/components/FileCard.standalone.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from "react"; -import { Card, Stack, Text, Group, Badge, Button, Box, Image, ThemeIcon } from "@mantine/core"; -import { useTranslation } from "react-i18next"; -import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf"; -import StorageIcon from "@mui/icons-material/Storage"; - -import { FileWithUrl } from "../types/file"; -import { getFileSize, getFileDate } from "../utils/fileUtils"; -import { useIndexedDBThumbnail } from "../hooks/useIndexedDBThumbnail"; - -interface FileCardProps { - file: FileWithUrl; - onRemove: () => void; - onDoubleClick?: () => void; -} - -const FileCard: React.FC = ({ file, onRemove, onDoubleClick }) => { - const { t } = useTranslation(); - const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file); - - return ( - - - - {thumb ? ( - PDF thumbnail - ) : isGenerating ? ( -
-
- Generating... -
- ) : ( -
- 100 * 1024 * 1024 ? "orange" : "red"} - size={60} - radius="sm" - style={{ display: "flex", alignItems: "center", justifyContent: "center" }} - > - - - {file.size > 100 * 1024 * 1024 && ( - Large File - )} -
- )} - - - - {file.name} - - - - - {getFileSize(file)} - - - {getFileDate(file)} - - {file.storedInIndexedDB && ( - } - > - DB - - )} - - - - - - ); -}; - -export default FileCard; \ No newline at end of file diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index 6ff6382d5..4981fa7a8 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -6,7 +6,7 @@ import { import { Dropzone } from '@mantine/dropzone'; import { useTranslation } from 'react-i18next'; import UploadFileIcon from '@mui/icons-material/UploadFile'; -import { useToolFileSelection, useProcessedFiles, useFileState, useFileManagement, useFileActions } from '../../contexts/FileContext'; +import { useToolFileSelection, useFileState, useFileManagement, useFileActions } from '../../contexts/FileContext'; import { FileOperation } from '../../types/fileContext'; import { fileStorage } from '../../services/fileStorage'; import { generateThumbnailForFile } from '../../utils/thumbnailUtils'; @@ -46,7 +46,6 @@ const FileEditor = ({ // Use optimized FileContext hooks const { state, selectors } = useFileState(); const { addFiles, removeFiles, reorderFiles } = useFileManagement(); - const processedFiles = useProcessedFiles(); // Now gets real processed files // Extract needed values from state (memoized to prevent infinite loops) const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]); @@ -63,7 +62,7 @@ const FileEditor = ({ selectedFileIdsRef.current = selectedFileIds; actionsRef.current = actions; - // Legacy compatibility for existing code - now actually updates context (completely stable) + // Wrapper for context file selection updates (stable) const setContextSelectedFiles = useCallback((fileIds: string[] | ((prev: string[]) => string[])) => { if (typeof fileIds === 'function') { // Handle callback pattern - get current state from ref diff --git a/frontend/src/components/shared/FileCard.tsx b/frontend/src/components/shared/FileCard.tsx index e4ff60eea..73ae01dba 100644 --- a/frontend/src/components/shared/FileCard.tsx +++ b/frontend/src/components/shared/FileCard.tsx @@ -6,12 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage"; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditIcon from "@mui/icons-material/Edit"; -import { FileWithUrl } from "../../types/file"; +import { FileRecord } from "../../types/fileContext"; import { getFileSize, getFileDate } from "../../utils/fileUtils"; import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail"; interface FileCardProps { - file: FileWithUrl; + file: File; + record?: FileRecord; onRemove: () => void; onDoubleClick?: () => void; onView?: () => void; @@ -21,9 +22,12 @@ interface FileCardProps { isSupported?: boolean; // Whether the file format is supported by the current tool } -const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => { +const FileCard = ({ file, record, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => { const { t } = useTranslation(); - const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file); + // Use record thumbnail if available, otherwise fall back to IndexedDB lookup + const fileMetadata = record ? { id: record.id, name: file.name, type: file.type, size: file.size, lastModified: file.lastModified } : null; + const { thumbnail: indexedDBThumb, isGenerating } = useIndexedDBThumbnail(fileMetadata); + const thumb = record?.thumbnailUrl || indexedDBThumb; const [isHovered, setIsHovered] = useState(false); return ( @@ -173,7 +177,7 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o {getFileDate(file)} - {file.storedInIndexedDB && ( + {record?.id && ( ; onRemove?: (index: number) => void; - onDoubleClick?: (file: FileWithUrl) => void; - onView?: (file: FileWithUrl) => void; - onEdit?: (file: FileWithUrl) => void; + onDoubleClick?: (item: { file: File; record?: FileRecord }) => void; + onView?: (item: { file: File; record?: FileRecord }) => void; + onEdit?: (item: { file: File; record?: FileRecord }) => void; onSelect?: (fileId: string) => void; selectedFiles?: string[]; showSearch?: boolean; @@ -46,19 +46,19 @@ const FileGrid = ({ const [sortBy, setSortBy] = useState('date'); // Filter files based on search term - const filteredFiles = files.filter(file => - file.name.toLowerCase().includes(searchTerm.toLowerCase()) + const filteredFiles = files.filter(item => + item.file.name.toLowerCase().includes(searchTerm.toLowerCase()) ); // Sort files const sortedFiles = [...filteredFiles].sort((a, b) => { switch (sortBy) { case 'date': - return (b.lastModified || 0) - (a.lastModified || 0); + return (b.file.lastModified || 0) - (a.file.lastModified || 0); case 'name': - return a.name.localeCompare(b.name); + return a.file.name.localeCompare(b.file.name); case 'size': - return (b.size || 0) - (a.size || 0); + return (b.file.size || 0) - (a.file.size || 0); default: return 0; } @@ -122,18 +122,19 @@ const FileGrid = ({ h="30rem" style={{ overflowY: "auto", width: "100%" }} > - {displayFiles.map((file, idx) => { - const fileId = file.id || file.name; - const originalIdx = files.findIndex(f => (f.id || f.name) === fileId); - const supported = isFileSupported ? isFileSupported(file.name) : true; + {displayFiles.map((item, idx) => { + const fileId = item.record?.id || item.file.name; + const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId); + const supported = isFileSupported ? isFileSupported(item.file.name) : true; return ( onRemove(originalIdx) : () => {}} - onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(file) : undefined} - onView={onView && supported ? () => onView(file) : undefined} - onEdit={onEdit && supported ? () => onEdit(file) : undefined} + onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(item) : undefined} + onView={onView && supported ? () => onView(item) : undefined} + onEdit={onEdit && supported ? () => onEdit(item) : undefined} isSelected={selectedFiles.includes(fileId)} onSelect={onSelect && supported ? () => onSelect(fileId) : undefined} isSupported={supported} diff --git a/frontend/src/components/shared/FilePickerModal.tsx b/frontend/src/components/shared/FilePickerModal.tsx index f489c5a11..1be0a4ec8 100644 --- a/frontend/src/components/shared/FilePickerModal.tsx +++ b/frontend/src/components/shared/FilePickerModal.tsx @@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next'; interface FilePickerModalProps { opened: boolean; onClose: () => void; - storedFiles: any[]; // Files from storage (FileWithUrl format) + storedFiles: any[]; // Files from storage (various formats supported) onSelectFiles: (selectedFiles: File[]) => void; } diff --git a/frontend/src/components/shared/FilePreview.tsx b/frontend/src/components/shared/FilePreview.tsx index d6acd5161..a13894040 100644 --- a/frontend/src/components/shared/FilePreview.tsx +++ b/frontend/src/components/shared/FilePreview.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Box } from '@mantine/core'; -import { FileWithUrl, FileMetadata } from '../../types/file'; +import { 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 | FileMetadata | null; + file: File | FileMetadata | null; thumbnail?: string | null; // Optional features @@ -21,7 +21,7 @@ export interface FilePreviewProps { isAnimating?: boolean; // Event handlers - onFileClick?: (file: File | FileWithUrl | FileMetadata | null) => void; + onFileClick?: (file: File | FileMetadata | null) => void; onPrevious?: () => void; onNext?: () => void; } diff --git a/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx b/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx index 0cf802a3a..56991e9d7 100644 --- a/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx +++ b/frontend/src/components/shared/filePreview/DocumentThumbnail.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Box, Center, Image } from '@mantine/core'; import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'; -import { FileWithUrl, FileMetadata } from '../../../types/file'; +import { FileMetadata } from '../../../types/file'; export interface DocumentThumbnailProps { - file: File | FileWithUrl | FileMetadata | null; + file: File | FileMetadata | null; thumbnail?: string | null; style?: React.CSSProperties; onClick?: () => void; diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx index 85ba8c976..c1a2b440a 100644 --- a/frontend/src/components/viewer/Viewer.tsx +++ b/frontend/src/components/viewer/Viewer.tsx @@ -13,7 +13,7 @@ import CloseIcon from "@mui/icons-material/Close"; import { useLocalStorage } from "@mantine/hooks"; import { fileStorage } from "../../services/fileStorage"; import SkeletonLoader from '../shared/SkeletonLoader'; -import { useFileState, useFileActions, useCurrentFile, useProcessedFiles } from "../../contexts/FileContext"; +import { useFileState, useFileActions, useCurrentFile } from "../../contexts/FileContext"; import { useFileWithUrl } from "../../hooks/useFileWithUrl"; @@ -152,11 +152,9 @@ const Viewer = ({ const { selectors } = useFileState(); const { actions } = useFileActions(); const currentFile = useCurrentFile(); - const processedFiles = useProcessedFiles(); - // Map legacy functions const getCurrentFile = () => currentFile.file; - const getCurrentProcessedFile = () => currentFile.file ? processedFiles.getProcessedFile(currentFile.file) : undefined; + const getCurrentProcessedFile = () => currentFile.record?.processedFile || undefined; const clearAllFiles = actions.clearAllFiles; const addFiles = actions.addFiles; const activeFiles = selectors.getFiles(); diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index 698496585..2c8b0bada 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -276,6 +276,5 @@ export { useSelectedFiles, // Primary API hooks for tools useFileContext, - useToolFileSelection, - useProcessedFiles + useToolFileSelection } from './file/fileHooks'; \ No newline at end of file diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index b39f04855..4ad397542 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react'; -import { FileWithUrl, FileMetadata } from '../types/file'; +import { FileMetadata } from '../types/file'; import { StoredFile } from '../services/fileStorage'; // Type for the context value - now contains everything directly diff --git a/frontend/src/contexts/file/fileHooks.ts b/frontend/src/contexts/file/fileHooks.ts index 01858ae91..6a7ccfa6f 100644 --- a/frontend/src/contexts/file/fileHooks.ts +++ b/frontend/src/contexts/file/fileHooks.ts @@ -218,36 +218,3 @@ export function useToolFileSelection() { ]); } -/** - * Hook for processed files (compatibility with old FileContext) - * Provides access to files with their processed metadata - */ -export function useProcessedFiles() { - const { state, selectors } = useFileState(); - - // Create a Map-like interface for backward compatibility - const compatibilityMap = useMemo(() => ({ - size: state.files.ids.length, - get: (file: File) => { - // Find file record by matching File object properties - const record = Object.values(state.files.byId).find(r => - r.name === file.name && r.size === file.size && r.lastModified === file.lastModified - ); - return record?.processedFile || null; - }, - has: (file: File) => { - // Find file record by matching File object properties - const record = Object.values(state.files.byId).find(r => - r.name === file.name && r.size === file.size && r.lastModified === file.lastModified - ); - return !!record?.processedFile; - }, - // Removed deprecated set method - }), [state.files.byId, state.files.ids.length]); - - return useMemo(() => ({ - processedFiles: compatibilityMap, - getProcessedFile: (file: File) => compatibilityMap.get(file), - // Removed deprecated updateProcessedFile method - }), [compatibilityMap]); -} \ No newline at end of file diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts index 718bc9370..eb4b5d6c2 100644 --- a/frontend/src/global.d.ts +++ b/frontend/src/global.d.ts @@ -1,6 +1,5 @@ declare module "../tools/Split"; declare module "../tools/Compress"; -declare module "../tools/Merge"; declare module "../components/PageEditor"; declare module "../components/Viewer"; declare module "*.js"; diff --git a/frontend/src/hooks/useFileManager.ts b/frontend/src/hooks/useFileManager.ts index b16fa6f4f..516e34460 100644 --- a/frontend/src/hooks/useFileManager.ts +++ b/frontend/src/hooks/useFileManager.ts @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; import { useIndexedDB } from '../contexts/IndexedDBContext'; -import { FileWithUrl, FileMetadata } from '../types/file'; +import { FileMetadata } from '../types/file'; import { generateThumbnailForFile } from '../utils/thumbnailUtils'; export const useFileManager = () => { diff --git a/frontend/src/tools/Merge.tsx b/frontend/src/tools/Merge.tsx deleted file mode 100644 index ecacda92a..000000000 --- a/frontend/src/tools/Merge.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core"; -import { useTranslation } from "react-i18next"; -import { FileWithUrl } from "../types/file"; -import { fileStorage } from "../services/fileStorage"; -import { useEndpointEnabled } from "../hooks/useEndpointConfig"; - -export interface MergePdfPanelProps { - files: FileWithUrl[]; - setDownloadUrl: (url: string) => void; - params: { - order: string; - removeDuplicates: boolean; - }; - updateParams: (newParams: Partial) => void; -} - -const MergePdfPanel: React.FC = ({ files, setDownloadUrl, params, updateParams }) => { - const { t } = useTranslation(); - const [selectedFiles, setSelectedFiles] = useState([]); - const [downloadUrl, setLocalDownloadUrl] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [errorMessage, setErrorMessage] = useState(null); - const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("merge-pdfs"); - - // Cleanup blob URL when component unmounts or new URL is set - useEffect(() => { - return () => { - if (downloadUrl && downloadUrl.startsWith('blob:')) { - URL.revokeObjectURL(downloadUrl); - } - }; - }, [downloadUrl]); - - useEffect(() => { - setSelectedFiles(files.map(() => true)); - }, [files]); - - const handleMerge = async () => { - const filesToMerge = files.filter((_, index) => selectedFiles[index]); - if (filesToMerge.length < 2) { - setErrorMessage(t("multiPdfPrompt")); // "Select PDFs (2+)" - return; - } - - const formData = new FormData(); - - // Handle IndexedDB files - for (const file of filesToMerge) { - if (!file.id) { - continue; // Skip files without an id - } - const storedFile = await fileStorage.getFile(file?.id); - if (storedFile) { - const blob = new Blob([storedFile.data], { type: storedFile.type }); - const actualFile = new File([blob], storedFile.name, { - type: storedFile.type, - lastModified: storedFile.lastModified, - }); - formData.append("fileInput", actualFile); - } - } - - setIsLoading(true); - setErrorMessage(null); - - try { - const response = await fetch("/api/v1/general/merge-pdfs", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Failed to merge PDFs: ${errorText}`); - } - - const blob = await response.blob(); - - // Clean up previous blob URL before setting new one - if (downloadUrl && downloadUrl.startsWith('blob:')) { - URL.revokeObjectURL(downloadUrl); - } - - const url = URL.createObjectURL(blob); - setDownloadUrl(url); - setLocalDownloadUrl(url); - } catch (error: any) { - setErrorMessage(error.message || "Unknown error occurred."); - } finally { - setIsLoading(false); - } - }; - - const handleCheckboxChange = (index: number) => { - setSelectedFiles((prev) => prev.map((selected, i) => (i === index ? !selected : selected))); - }; - - const selectedCount = selectedFiles.filter(Boolean).length; - - const { order, removeDuplicates } = params; - - if (endpointLoading) { - return ( - - - - {t("loading", "Loading...")} - - - ); - } - - if (endpointEnabled === false) { - return ( - - - {t("endpointDisabled", "This feature is currently disabled.")} - - - ); - } - - return ( - - - {t("merge.header")} - - - {files.map((file, index) => ( - - handleCheckboxChange(index)} /> - {file.name} - - ))} - - {selectedCount < 2 && ( - - {t("multiPdfPrompt")} - - )} - - {errorMessage && ( - - {errorMessage} - - )} - {downloadUrl && ( - - )} - updateParams({ removeDuplicates: !removeDuplicates })} - /> - - ); -}; - -export default MergePdfPanel; diff --git a/frontend/src/types/file.ts b/frontend/src/types/file.ts index d4b44e803..96e507523 100644 --- a/frontend/src/types/file.ts +++ b/frontend/src/types/file.ts @@ -3,16 +3,6 @@ * FileContext uses pure File objects with separate ID tracking */ -/** - * @deprecated Use pure File objects with FileContext for ID management - * This interface exists for backward compatibility only - */ -export interface FileWithUrl extends File { - id: string; // Required UUID from FileContext - url?: string; // Blob URL for display - thumbnail?: string; - storedInIndexedDB?: boolean; -} /** * File metadata for efficient operations without loading full file data diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 31ac39d2f..10fa49b67 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -80,15 +80,10 @@ export function createQuickKey(file: File): string { return `${file.name}|${file.size}|${file.lastModified}`; } -// Legacy support - now just delegates to createFileId -export function createStableFileId(file: File): FileId { - // Don't mutate File objects - always return new UUID - return createFileId(); -} export function toFileRecord(file: File, id?: FileId): FileRecord { - const fileId = id || createStableFileId(file); + const fileId = id || createFileId(); return { id: fileId, name: file.name, diff --git a/frontend/src/utils/storageUtils.ts b/frontend/src/utils/storageUtils.ts index def05b96d..14ae78fee 100644 --- a/frontend/src/utils/storageUtils.ts +++ b/frontend/src/utils/storageUtils.ts @@ -1,5 +1,4 @@ import { StorageStats } from "../services/fileStorage"; -import { FileWithUrl } from "../types/file"; /** * Storage operation types for incremental updates @@ -12,7 +11,7 @@ export type StorageOperation = 'add' | 'remove' | 'clear'; export function updateStorageStatsIncremental( currentStats: StorageStats, operation: StorageOperation, - files: FileWithUrl[] = [] + files: File[] = [] ): StorageStats { const filesSizeTotal = files.reduce((total, file) => total + file.size, 0);