mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Remove obsolete filewithurl interface
This commit is contained in:
parent
f1246e3ab0
commit
d29b203bed
@ -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;
|
|
@ -6,7 +6,7 @@ import {
|
|||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
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 { FileOperation } from '../../types/fileContext';
|
||||||
import { fileStorage } from '../../services/fileStorage';
|
import { fileStorage } from '../../services/fileStorage';
|
||||||
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../../utils/thumbnailUtils';
|
||||||
@ -46,7 +46,6 @@ const FileEditor = ({
|
|||||||
// Use optimized FileContext hooks
|
// Use optimized FileContext hooks
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
const { addFiles, removeFiles, reorderFiles } = useFileManagement();
|
const { addFiles, removeFiles, reorderFiles } = useFileManagement();
|
||||||
const processedFiles = useProcessedFiles(); // Now gets real processed files
|
|
||||||
|
|
||||||
// Extract needed values from state (memoized to prevent infinite loops)
|
// Extract needed values from state (memoized to prevent infinite loops)
|
||||||
const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]);
|
const activeFiles = useMemo(() => selectors.getFiles(), [selectors.getFilesSignature()]);
|
||||||
@ -63,7 +62,7 @@ const FileEditor = ({
|
|||||||
selectedFileIdsRef.current = selectedFileIds;
|
selectedFileIdsRef.current = selectedFileIds;
|
||||||
actionsRef.current = actions;
|
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[])) => {
|
const setContextSelectedFiles = useCallback((fileIds: string[] | ((prev: string[]) => string[])) => {
|
||||||
if (typeof fileIds === 'function') {
|
if (typeof fileIds === 'function') {
|
||||||
// Handle callback pattern - get current state from ref
|
// Handle callback pattern - get current state from ref
|
||||||
|
@ -6,12 +6,13 @@ import StorageIcon from "@mui/icons-material/Storage";
|
|||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
|
||||||
import { FileWithUrl } from "../../types/file";
|
import { FileRecord } from "../../types/fileContext";
|
||||||
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
||||||
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
||||||
|
|
||||||
interface FileCardProps {
|
interface FileCardProps {
|
||||||
file: FileWithUrl;
|
file: File;
|
||||||
|
record?: FileRecord;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
onView?: () => void;
|
onView?: () => void;
|
||||||
@ -21,9 +22,12 @@ interface FileCardProps {
|
|||||||
isSupported?: boolean; // Whether the file format is supported by the current tool
|
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 { 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);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -173,7 +177,7 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
|
|||||||
<Badge color="blue" variant="light" size="sm">
|
<Badge color="blue" variant="light" size="sm">
|
||||||
{getFileDate(file)}
|
{getFileDate(file)}
|
||||||
</Badge>
|
</Badge>
|
||||||
{file.storedInIndexedDB && (
|
{record?.id && (
|
||||||
<Badge
|
<Badge
|
||||||
color="green"
|
color="green"
|
||||||
variant="light"
|
variant="light"
|
||||||
|
@ -4,14 +4,14 @@ import { useTranslation } from "react-i18next";
|
|||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import SortIcon from "@mui/icons-material/Sort";
|
import SortIcon from "@mui/icons-material/Sort";
|
||||||
import FileCard from "./FileCard";
|
import FileCard from "./FileCard";
|
||||||
import { FileWithUrl } from "../../types/file";
|
import { FileRecord } from "../../types/fileContext";
|
||||||
|
|
||||||
interface FileGridProps {
|
interface FileGridProps {
|
||||||
files: FileWithUrl[];
|
files: Array<{ file: File; record?: FileRecord }>;
|
||||||
onRemove?: (index: number) => void;
|
onRemove?: (index: number) => void;
|
||||||
onDoubleClick?: (file: FileWithUrl) => void;
|
onDoubleClick?: (item: { file: File; record?: FileRecord }) => void;
|
||||||
onView?: (file: FileWithUrl) => void;
|
onView?: (item: { file: File; record?: FileRecord }) => void;
|
||||||
onEdit?: (file: FileWithUrl) => void;
|
onEdit?: (item: { file: File; record?: FileRecord }) => void;
|
||||||
onSelect?: (fileId: string) => void;
|
onSelect?: (fileId: string) => void;
|
||||||
selectedFiles?: string[];
|
selectedFiles?: string[];
|
||||||
showSearch?: boolean;
|
showSearch?: boolean;
|
||||||
@ -46,19 +46,19 @@ const FileGrid = ({
|
|||||||
const [sortBy, setSortBy] = useState<SortOption>('date');
|
const [sortBy, setSortBy] = useState<SortOption>('date');
|
||||||
|
|
||||||
// Filter files based on search term
|
// Filter files based on search term
|
||||||
const filteredFiles = files.filter(file =>
|
const filteredFiles = files.filter(item =>
|
||||||
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
item.file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sort files
|
// Sort files
|
||||||
const sortedFiles = [...filteredFiles].sort((a, b) => {
|
const sortedFiles = [...filteredFiles].sort((a, b) => {
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'date':
|
case 'date':
|
||||||
return (b.lastModified || 0) - (a.lastModified || 0);
|
return (b.file.lastModified || 0) - (a.file.lastModified || 0);
|
||||||
case 'name':
|
case 'name':
|
||||||
return a.name.localeCompare(b.name);
|
return a.file.name.localeCompare(b.file.name);
|
||||||
case 'size':
|
case 'size':
|
||||||
return (b.size || 0) - (a.size || 0);
|
return (b.file.size || 0) - (a.file.size || 0);
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -122,18 +122,19 @@ const FileGrid = ({
|
|||||||
h="30rem"
|
h="30rem"
|
||||||
style={{ overflowY: "auto", width: "100%" }}
|
style={{ overflowY: "auto", width: "100%" }}
|
||||||
>
|
>
|
||||||
{displayFiles.map((file, idx) => {
|
{displayFiles.map((item, idx) => {
|
||||||
const fileId = file.id || file.name;
|
const fileId = item.record?.id || item.file.name;
|
||||||
const originalIdx = files.findIndex(f => (f.id || f.name) === fileId);
|
const originalIdx = files.findIndex(f => (f.record?.id || f.file.name) === fileId);
|
||||||
const supported = isFileSupported ? isFileSupported(file.name) : true;
|
const supported = isFileSupported ? isFileSupported(item.file.name) : true;
|
||||||
return (
|
return (
|
||||||
<FileCard
|
<FileCard
|
||||||
key={fileId + idx}
|
key={fileId + idx}
|
||||||
file={file}
|
file={item.file}
|
||||||
|
record={item.record}
|
||||||
onRemove={onRemove ? () => onRemove(originalIdx) : () => {}}
|
onRemove={onRemove ? () => onRemove(originalIdx) : () => {}}
|
||||||
onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(file) : undefined}
|
onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(item) : undefined}
|
||||||
onView={onView && supported ? () => onView(file) : undefined}
|
onView={onView && supported ? () => onView(item) : undefined}
|
||||||
onEdit={onEdit && supported ? () => onEdit(file) : undefined}
|
onEdit={onEdit && supported ? () => onEdit(item) : undefined}
|
||||||
isSelected={selectedFiles.includes(fileId)}
|
isSelected={selectedFiles.includes(fileId)}
|
||||||
onSelect={onSelect && supported ? () => onSelect(fileId) : undefined}
|
onSelect={onSelect && supported ? () => onSelect(fileId) : undefined}
|
||||||
isSupported={supported}
|
isSupported={supported}
|
||||||
|
@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
interface FilePickerModalProps {
|
interface FilePickerModalProps {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
storedFiles: any[]; // Files from storage (FileWithUrl format)
|
storedFiles: any[]; // Files from storage (various formats supported)
|
||||||
onSelectFiles: (selectedFiles: File[]) => void;
|
onSelectFiles: (selectedFiles: File[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box } from '@mantine/core';
|
import { Box } from '@mantine/core';
|
||||||
import { FileWithUrl, FileMetadata } from '../../types/file';
|
import { FileMetadata } from '../../types/file';
|
||||||
import DocumentThumbnail from './filePreview/DocumentThumbnail';
|
import DocumentThumbnail from './filePreview/DocumentThumbnail';
|
||||||
import DocumentStack from './filePreview/DocumentStack';
|
import DocumentStack from './filePreview/DocumentStack';
|
||||||
import HoverOverlay from './filePreview/HoverOverlay';
|
import HoverOverlay from './filePreview/HoverOverlay';
|
||||||
@ -8,7 +8,7 @@ import NavigationArrows from './filePreview/NavigationArrows';
|
|||||||
|
|
||||||
export interface FilePreviewProps {
|
export interface FilePreviewProps {
|
||||||
// Core file data
|
// Core file data
|
||||||
file: File | FileWithUrl | FileMetadata | null;
|
file: File | FileMetadata | null;
|
||||||
thumbnail?: string | null;
|
thumbnail?: string | null;
|
||||||
|
|
||||||
// Optional features
|
// Optional features
|
||||||
@ -21,7 +21,7 @@ export interface FilePreviewProps {
|
|||||||
isAnimating?: boolean;
|
isAnimating?: boolean;
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers
|
||||||
onFileClick?: (file: File | FileWithUrl | FileMetadata | null) => void;
|
onFileClick?: (file: File | FileMetadata | null) => void;
|
||||||
onPrevious?: () => void;
|
onPrevious?: () => void;
|
||||||
onNext?: () => void;
|
onNext?: () => void;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Center, Image } from '@mantine/core';
|
import { Box, Center, Image } from '@mantine/core';
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||||
import { FileWithUrl, FileMetadata } from '../../../types/file';
|
import { FileMetadata } from '../../../types/file';
|
||||||
|
|
||||||
export interface DocumentThumbnailProps {
|
export interface DocumentThumbnailProps {
|
||||||
file: File | FileWithUrl | FileMetadata | null;
|
file: File | FileMetadata | null;
|
||||||
thumbnail?: string | null;
|
thumbnail?: string | null;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
@ -13,7 +13,7 @@ import CloseIcon from "@mui/icons-material/Close";
|
|||||||
import { useLocalStorage } from "@mantine/hooks";
|
import { useLocalStorage } from "@mantine/hooks";
|
||||||
import { fileStorage } from "../../services/fileStorage";
|
import { fileStorage } from "../../services/fileStorage";
|
||||||
import SkeletonLoader from '../shared/SkeletonLoader';
|
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";
|
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
|
||||||
|
|
||||||
|
|
||||||
@ -152,11 +152,9 @@ const Viewer = ({
|
|||||||
const { selectors } = useFileState();
|
const { selectors } = useFileState();
|
||||||
const { actions } = useFileActions();
|
const { actions } = useFileActions();
|
||||||
const currentFile = useCurrentFile();
|
const currentFile = useCurrentFile();
|
||||||
const processedFiles = useProcessedFiles();
|
|
||||||
|
|
||||||
// Map legacy functions
|
|
||||||
const getCurrentFile = () => currentFile.file;
|
const getCurrentFile = () => currentFile.file;
|
||||||
const getCurrentProcessedFile = () => currentFile.file ? processedFiles.getProcessedFile(currentFile.file) : undefined;
|
const getCurrentProcessedFile = () => currentFile.record?.processedFile || undefined;
|
||||||
const clearAllFiles = actions.clearAllFiles;
|
const clearAllFiles = actions.clearAllFiles;
|
||||||
const addFiles = actions.addFiles;
|
const addFiles = actions.addFiles;
|
||||||
const activeFiles = selectors.getFiles();
|
const activeFiles = selectors.getFiles();
|
||||||
|
@ -276,6 +276,5 @@ export {
|
|||||||
useSelectedFiles,
|
useSelectedFiles,
|
||||||
// Primary API hooks for tools
|
// Primary API hooks for tools
|
||||||
useFileContext,
|
useFileContext,
|
||||||
useToolFileSelection,
|
useToolFileSelection
|
||||||
useProcessedFiles
|
|
||||||
} from './file/fileHooks';
|
} from './file/fileHooks';
|
@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useContext, useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
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';
|
import { StoredFile } from '../services/fileStorage';
|
||||||
|
|
||||||
// Type for the context value - now contains everything directly
|
// Type for the context value - now contains everything directly
|
||||||
|
@ -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]);
|
|
||||||
}
|
|
1
frontend/src/global.d.ts
vendored
1
frontend/src/global.d.ts
vendored
@ -1,6 +1,5 @@
|
|||||||
declare module "../tools/Split";
|
declare module "../tools/Split";
|
||||||
declare module "../tools/Compress";
|
declare module "../tools/Compress";
|
||||||
declare module "../tools/Merge";
|
|
||||||
declare module "../components/PageEditor";
|
declare module "../components/PageEditor";
|
||||||
declare module "../components/Viewer";
|
declare module "../components/Viewer";
|
||||||
declare module "*.js";
|
declare module "*.js";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useIndexedDB } from '../contexts/IndexedDBContext';
|
import { useIndexedDB } from '../contexts/IndexedDBContext';
|
||||||
import { FileWithUrl, FileMetadata } from '../types/file';
|
import { FileMetadata } from '../types/file';
|
||||||
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../utils/thumbnailUtils';
|
||||||
|
|
||||||
export const useFileManager = () => {
|
export const useFileManager = () => {
|
||||||
|
@ -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;
|
|
@ -3,16 +3,6 @@
|
|||||||
* FileContext uses pure File objects with separate ID tracking
|
* 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
|
* File metadata for efficient operations without loading full file data
|
||||||
|
@ -80,15 +80,10 @@ export function createQuickKey(file: File): string {
|
|||||||
return `${file.name}|${file.size}|${file.lastModified}`;
|
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 {
|
export function toFileRecord(file: File, id?: FileId): FileRecord {
|
||||||
const fileId = id || createStableFileId(file);
|
const fileId = id || createFileId();
|
||||||
return {
|
return {
|
||||||
id: fileId,
|
id: fileId,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { StorageStats } from "../services/fileStorage";
|
import { StorageStats } from "../services/fileStorage";
|
||||||
import { FileWithUrl } from "../types/file";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Storage operation types for incremental updates
|
* Storage operation types for incremental updates
|
||||||
@ -12,7 +11,7 @@ export type StorageOperation = 'add' | 'remove' | 'clear';
|
|||||||
export function updateStorageStatsIncremental(
|
export function updateStorageStatsIncremental(
|
||||||
currentStats: StorageStats,
|
currentStats: StorageStats,
|
||||||
operation: StorageOperation,
|
operation: StorageOperation,
|
||||||
files: FileWithUrl[] = []
|
files: File[] = []
|
||||||
): StorageStats {
|
): StorageStats {
|
||||||
const filesSizeTotal = files.reduce((total, file) => total + file.size, 0);
|
const filesSizeTotal = files.reduce((total, file) => total + file.size, 0);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user