Remove obsolete filewithurl interface

This commit is contained in:
Reece Browne 2025-08-20 16:36:02 +01:00
parent f1246e3ab0
commit d29b203bed
17 changed files with 43 additions and 392 deletions

View File

@ -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<FileCardProps> = ({ file, onRemove, onDoubleClick }) => {
const { t } = useTranslation();
const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file);
return (
<Card
shadow="xs"
radius="md"
withBorder
p="xs"
style={{
width: 225,
minWidth: 180,
maxWidth: 260,
cursor: onDoubleClick ? "pointer" : undefined
}}
onDoubleClick={onDoubleClick}
>
<Stack gap={6} align="center">
<Box
style={{
border: "2px solid #e0e0e0",
borderRadius: 8,
width: 90,
height: 120,
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: "0 auto",
background: "#fafbfc",
}}
>
{thumb ? (
<Image
src={thumb}
alt="PDF thumbnail"
height={110}
width={80}
fit="contain"
radius="sm"
/>
) : isGenerating ? (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
width: 20,
height: 20,
border: '2px solid #ddd',
borderTop: '2px solid #666',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
marginBottom: 8
}} />
<Text size="xs" c="dimmed">Generating...</Text>
</div>
) : (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}>
<ThemeIcon
variant="light"
color={file.size > 100 * 1024 * 1024 ? "orange" : "red"}
size={60}
radius="sm"
style={{ display: "flex", alignItems: "center", justifyContent: "center" }}
>
<PictureAsPdfIcon style={{ fontSize: 40 }} />
</ThemeIcon>
{file.size > 100 * 1024 * 1024 && (
<Text size="xs" c="dimmed" mt={4}>Large File</Text>
)}
</div>
)}
</Box>
<Text fw={500} size="sm" lineClamp={1} ta="center">
{file.name}
</Text>
<Group gap="xs" justify="center">
<Badge color="gray" variant="light" size="sm">
{getFileSize(file)}
</Badge>
<Badge color="blue" variant="light" size="sm">
{getFileDate(file)}
</Badge>
{file.storedInIndexedDB && (
<Badge
color="green"
variant="light"
size="sm"
leftSection={<StorageIcon style={{ fontSize: 12 }} />}
>
DB
</Badge>
)}
</Group>
<Button
color="red"
size="xs"
variant="light"
onClick={onRemove}
mt={4}
>
{t("delete", "Remove")}
</Button>
</Stack>
</Card>
);
};
export default FileCard;

View File

@ -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

View File

@ -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
<Badge color="blue" variant="light" size="sm">
{getFileDate(file)}
</Badge>
{file.storedInIndexedDB && (
{record?.id && (
<Badge
color="green"
variant="light"

View File

@ -4,14 +4,14 @@ import { useTranslation } from "react-i18next";
import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort";
import FileCard from "./FileCard";
import { FileWithUrl } from "../../types/file";
import { FileRecord } from "../../types/fileContext";
interface FileGridProps {
files: FileWithUrl[];
files: Array<{ file: File; record?: FileRecord }>;
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<SortOption>('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 (
<FileCard
key={fileId + idx}
file={file}
file={item.file}
record={item.record}
onRemove={onRemove ? () => 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}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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();

View File

@ -276,6 +276,5 @@ export {
useSelectedFiles,
// Primary API hooks for tools
useFileContext,
useToolFileSelection,
useProcessedFiles
useToolFileSelection
} from './file/fileHooks';

View File

@ -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

View File

@ -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]);
}

View File

@ -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";

View File

@ -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 = () => {

View File

@ -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<MergePdfPanelProps["params"]>) => void;
}
const MergePdfPanel: React.FC<MergePdfPanelProps> = ({ files, setDownloadUrl, params, updateParams }) => {
const { t } = useTranslation();
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(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 (
<Stack align="center" justify="center" h={200}>
<Loader size="md" />
<Text size="sm" c="dimmed">
{t("loading", "Loading...")}
</Text>
</Stack>
);
}
if (endpointEnabled === false) {
return (
<Stack align="center" justify="center" h={200}>
<Alert color="red" title={t("error._value", "Error")} variant="light">
{t("endpointDisabled", "This feature is currently disabled.")}
</Alert>
</Stack>
);
}
return (
<Stack>
<Text fw={500} size="lg">
{t("merge.header")}
</Text>
<Stack gap={4}>
{files.map((file, index) => (
<Group key={index} gap="xs">
<Checkbox checked={selectedFiles[index] || false} onChange={() => handleCheckboxChange(index)} />
<Text size="sm">{file.name}</Text>
</Group>
))}
</Stack>
{selectedCount < 2 && (
<Text size="sm" c="red">
{t("multiPdfPrompt")}
</Text>
)}
<Button onClick={handleMerge} loading={isLoading} disabled={selectedCount < 2 || isLoading} mt="md">
{t("merge.submit")}
</Button>
{errorMessage && (
<Alert color="red" mt="sm">
{errorMessage}
</Alert>
)}
{downloadUrl && (
<Button component="a" href={downloadUrl} download="merged.pdf" color="green" variant="light" mt="md">
{t("downloadPdf")}
</Button>
)}
<Checkbox
label={t("merge.removeCertSign")}
checked={removeDuplicates}
onChange={() => updateParams({ removeDuplicates: !removeDuplicates })}
/>
</Stack>
);
};
export default MergePdfPanel;

View File

@ -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

View File

@ -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,

View File

@ -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);