mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +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 { 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
|
||||
|
@ -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"
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -276,6 +276,5 @@ export {
|
||||
useSelectedFiles,
|
||||
// Primary API hooks for tools
|
||||
useFileContext,
|
||||
useToolFileSelection,
|
||||
useProcessedFiles
|
||||
useToolFileSelection
|
||||
} from './file/fileHooks';
|
@ -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
|
||||
|
@ -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/Compress";
|
||||
declare module "../tools/Merge";
|
||||
declare module "../components/PageEditor";
|
||||
declare module "../components/Viewer";
|
||||
declare module "*.js";
|
||||
|
@ -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 = () => {
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user