Supported files & single file auto label

This commit is contained in:
Connor Yoh 2025-08-01 12:14:24 +01:00
parent bbf92afa36
commit 695b3766ac
9 changed files with 148 additions and 26 deletions

View File

@ -12,6 +12,7 @@ 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';
import { zipFileService } from '../../services/zipFileService'; import { zipFileService } from '../../services/zipFileService';
import { detectFileExtension } from '../../utils/fileUtils';
import styles from '../pageEditor/PageEditor.module.css'; import styles from '../pageEditor/PageEditor.module.css';
import FileThumbnail from '../pageEditor/FileThumbnail'; import FileThumbnail from '../pageEditor/FileThumbnail';
import DragDropGrid from '../pageEditor/DragDropGrid'; import DragDropGrid from '../pageEditor/DragDropGrid';
@ -34,6 +35,7 @@ interface FileEditorProps {
toolMode?: boolean; toolMode?: boolean;
showUpload?: boolean; showUpload?: boolean;
showBulkActions?: boolean; showBulkActions?: boolean;
supportedExtensions?: string[];
} }
const FileEditor = ({ const FileEditor = ({
@ -41,10 +43,17 @@ const FileEditor = ({
onMergeFiles, onMergeFiles,
toolMode = false, toolMode = false,
showUpload = true, showUpload = true,
showBulkActions = true showBulkActions = true,
supportedExtensions = ["pdf"]
}: FileEditorProps) => { }: FileEditorProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
// Utility function to check if a file extension is supported
const isFileSupported = useCallback((fileName: string): boolean => {
const extension = detectFileExtension(fileName);
return extension ? supportedExtensions.includes(extension) : false;
}, [supportedExtensions]);
// Get file context // Get file context
const fileContext = useFileContext(); const fileContext = useFileContext();
const { const {
@ -807,6 +816,7 @@ const FileEditor = ({
onSplitFile={handleSplitFile} onSplitFile={handleSplitFile}
onSetStatus={setStatus} onSetStatus={setStatus}
toolMode={toolMode} toolMode={toolMode}
isSupported={isFileSupported(file.name)}
/> />
)} )}
renderSplitMarker={(file, index) => ( renderSplitMarker={(file, index) => (

View File

@ -18,9 +18,10 @@ interface FileCardProps {
onEdit?: () => void; onEdit?: () => void;
isSelected?: boolean; isSelected?: boolean;
onSelect?: () => void; onSelect?: () => void;
isSupported?: boolean; // Whether the file format is supported by the current tool
} }
const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect }: FileCardProps) => { const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, onSelect, isSupported = true }: FileCardProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file); const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@ -35,10 +36,12 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
width: 225, width: 225,
minWidth: 180, minWidth: 180,
maxWidth: 260, maxWidth: 260,
cursor: onDoubleClick ? "pointer" : undefined, cursor: onDoubleClick && isSupported ? "pointer" : undefined,
position: 'relative', position: 'relative',
border: isSelected ? '2px solid var(--mantine-color-blue-6)' : undefined, border: isSelected ? '2px solid var(--mantine-color-blue-6)' : undefined,
backgroundColor: isSelected ? 'var(--mantine-color-blue-0)' : undefined backgroundColor: isSelected ? 'var(--mantine-color-blue-0)' : undefined,
opacity: isSupported ? 1 : 0.5,
filter: isSupported ? 'none' : 'grayscale(50%)'
}} }}
onDoubleClick={onDoubleClick} onDoubleClick={onDoubleClick}
onMouseEnter={() => setIsHovered(true)} onMouseEnter={() => setIsHovered(true)}
@ -180,6 +183,11 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
DB DB
</Badge> </Badge>
)} )}
{!isSupported && (
<Badge color="orange" variant="filled" size="sm">
{t("fileManager.unsupported", "Unsupported")}
</Badge>
)}
</Group> </Group>
<Button <Button

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Text, Checkbox, Tooltip, ActionIcon, Badge, Modal } from '@mantine/core'; import { Text, Checkbox, Tooltip, ActionIcon, Badge, Modal } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import VisibilityIcon from '@mui/icons-material/Visibility'; import VisibilityIcon from '@mui/icons-material/Visibility';
import HistoryIcon from '@mui/icons-material/History'; import HistoryIcon from '@mui/icons-material/History';
@ -37,6 +38,7 @@ interface FileThumbnailProps {
onViewFile: (fileId: string) => void; onViewFile: (fileId: string) => void;
onSetStatus: (status: string) => void; onSetStatus: (status: string) => void;
toolMode?: boolean; toolMode?: boolean;
isSupported?: boolean;
} }
const FileThumbnail = ({ const FileThumbnail = ({
@ -60,7 +62,9 @@ const FileThumbnail = ({
onViewFile, onViewFile,
onSetStatus, onSetStatus,
toolMode = false, toolMode = false,
isSupported = true,
}: FileThumbnailProps) => { }: FileThumbnailProps) => {
const { t } = useTranslation();
const [showHistory, setShowHistory] = useState(false); const [showHistory, setShowHistory] = useState(false);
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
@ -107,7 +111,9 @@ const FileThumbnail = ({
} }
return 'translateX(0)'; return 'translateX(0)';
})(), })(),
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out' transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out',
opacity: isSupported ? 1 : 0.5,
filter: isSupported ? 'none' : 'grayscale(50%)'
}} }}
draggable draggable
onDragStart={() => onDragStart(file.id)} onDragStart={() => onDragStart(file.id)}
@ -142,9 +148,12 @@ const FileThumbnail = ({
checked={selectedFiles.includes(file.id)} checked={selectedFiles.includes(file.id)}
onChange={(event) => { onChange={(event) => {
event.stopPropagation(); event.stopPropagation();
onToggleFile(file.id); if (isSupported) {
onToggleFile(file.id);
}
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
disabled={!isSupported}
size="sm" size="sm"
/> />
</div> </div>
@ -195,6 +204,23 @@ const FileThumbnail = ({
{file.pageCount} pages {file.pageCount} pages
</Badge> </Badge>
{/* Unsupported badge */}
{!isSupported && (
<Badge
size="sm"
variant="filled"
color="orange"
style={{
position: 'absolute',
top: 8,
right: selectionMode ? 48 : 8, // Avoid overlap with checkbox
zIndex: 3,
}}
>
{t("fileManager.unsupported", "Unsupported")}
</Badge>
)}
{/* File name overlay */} {/* File name overlay */}
<Text <Text
className={styles.pageNumber} className={styles.pageNumber}
@ -240,7 +266,7 @@ const FileThumbnail = ({
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}} }}
> >
{!toolMode && ( {!toolMode && isSupported && (
<> <>
<Tooltip label="View File"> <Tooltip label="View File">
<ActionIcon <ActionIcon

View File

@ -20,6 +20,7 @@ interface FileGridProps {
onShowAll?: () => void; onShowAll?: () => void;
showingAll?: boolean; showingAll?: boolean;
onDeleteAll?: () => void; onDeleteAll?: () => void;
isFileSupported?: (fileName: string) => boolean; // Function to check if file is supported
} }
type SortOption = 'date' | 'name' | 'size'; type SortOption = 'date' | 'name' | 'size';
@ -37,7 +38,8 @@ const FileGrid = ({
maxDisplay, maxDisplay,
onShowAll, onShowAll,
showingAll = false, showingAll = false,
onDeleteAll onDeleteAll,
isFileSupported
}: FileGridProps) => { }: FileGridProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@ -123,16 +125,18 @@ const FileGrid = ({
{displayFiles.map((file, idx) => { {displayFiles.map((file, idx) => {
const fileId = file.id || file.name; const fileId = file.id || file.name;
const originalIdx = files.findIndex(f => (f.id || f.name) === fileId); const originalIdx = files.findIndex(f => (f.id || f.name) === fileId);
const supported = isFileSupported ? isFileSupported(file.name) : true;
return ( return (
<FileCard <FileCard
key={fileId + idx} key={fileId + idx}
file={file} file={file}
onRemove={onRemove ? () => onRemove(originalIdx) : undefined} onRemove={onRemove ? () => onRemove(originalIdx) : undefined}
onDoubleClick={onDoubleClick ? () => onDoubleClick(file) : undefined} onDoubleClick={onDoubleClick && supported ? () => onDoubleClick(file) : undefined}
onView={onView ? () => onView(file) : undefined} onView={onView && supported ? () => onView(file) : undefined}
onEdit={onEdit ? () => onEdit(file) : undefined} onEdit={onEdit && supported ? () => onEdit(file) : undefined}
isSelected={selectedFiles.includes(fileId)} isSelected={selectedFiles.includes(fileId)}
onSelect={onSelect ? () => onSelect(fileId) : undefined} onSelect={onSelect && supported ? () => onSelect(fileId) : undefined}
isSupported={supported}
/> />
); );
})} })}

View File

@ -5,6 +5,7 @@ import UploadFileIcon from '@mui/icons-material/UploadFile';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { fileStorage } from '../../services/fileStorage'; import { fileStorage } from '../../services/fileStorage';
import { FileWithUrl } from '../../types/file'; import { FileWithUrl } from '../../types/file';
import { detectFileExtension } from '../../utils/fileUtils';
import FileGrid from './FileGrid'; import FileGrid from './FileGrid';
import MultiSelectControls from './MultiSelectControls'; import MultiSelectControls from './MultiSelectControls';
import { useFileManager } from '../../hooks/useFileManager'; import { useFileManager } from '../../hooks/useFileManager';
@ -20,6 +21,7 @@ interface FileUploadSelectorProps {
onFileSelect?: (file: File) => void; onFileSelect?: (file: File) => void;
onFilesSelect: (files: File[]) => void; onFilesSelect: (files: File[]) => void;
accept?: string[]; accept?: string[];
supportedExtensions?: string[]; // Extensions this tool supports (e.g., ['pdf', 'jpg', 'png'])
// Loading state // Loading state
loading?: boolean; loading?: boolean;
@ -38,6 +40,7 @@ const FileUploadSelector = ({
onFileSelect, onFileSelect,
onFilesSelect, onFilesSelect,
accept = ["application/pdf", "application/zip", "application/x-zip-compressed"], accept = ["application/pdf", "application/zip", "application/x-zip-compressed"],
supportedExtensions = ["pdf"], // Default to PDF only for most tools
loading = false, loading = false,
disabled = false, disabled = false,
showRecentFiles = true, showRecentFiles = true,
@ -51,6 +54,12 @@ const FileUploadSelector = ({
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, createFileSelectionHandlers } = useFileManager(); const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, createFileSelectionHandlers } = useFileManager();
// Utility function to check if a file extension is supported
const isFileSupported = useCallback((fileName: string): boolean => {
const extension = detectFileExtension(fileName);
return extension ? supportedExtensions.includes(extension) : false;
}, [supportedExtensions]);
const refreshRecentFiles = useCallback(async () => { const refreshRecentFiles = useCallback(async () => {
const files = await loadRecentFiles(); const files = await loadRecentFiles();
setRecentFiles(files); setRecentFiles(files);
@ -227,6 +236,7 @@ const FileUploadSelector = ({
selectedFiles={selectedFiles} selectedFiles={selectedFiles}
showSearch={true} showSearch={true}
showSort={true} showSort={true}
isFileSupported={isFileSupported}
onDeleteAll={async () => { onDeleteAll={async () => {
await Promise.all(recentFiles.map(async (file) => { await Promise.all(recentFiles.map(async (file) => {
await fileStorage.deleteFile(file.id || file.name); await fileStorage.deleteFile(file.id || file.name);

View File

@ -64,20 +64,35 @@ const ConvertSettings = ({
// Enhanced FROM options with endpoint availability // Enhanced FROM options with endpoint availability
const enhancedFromOptions = useMemo(() => { const enhancedFromOptions = useMemo(() => {
return FROM_FORMAT_OPTIONS.map(option => { const baseOptions = FROM_FORMAT_OPTIONS.map(option => {
// Check if this source format has any available conversions // Check if this source format has any available conversions
const availableConversions = getAvailableToExtensions(option.value) || []; const availableConversions = getAvailableToExtensions(option.value) || [];
const hasAvailableConversions = availableConversions.some(targetOption => const hasAvailableConversions = availableConversions.some(targetOption =>
isConversionAvailable(option.value, targetOption.value) isConversionAvailable(option.value, targetOption.value)
); );
return { return {
...option, ...option,
enabled: hasAvailableConversions enabled: hasAvailableConversions
}; };
}); });
}, [getAvailableToExtensions, endpointStatus]);
// Add dynamic format option if current selection is a file-<extension> format
if (parameters.fromExtension && parameters.fromExtension.startsWith('file-')) {
const extension = parameters.fromExtension.replace('file-', '');
const dynamicOption = {
value: parameters.fromExtension,
label: extension.toUpperCase(),
group: 'File',
enabled: true
};
// Add the dynamic option at the beginning
return [dynamicOption, ...baseOptions];
}
return baseOptions;
}, [getAvailableToExtensions, endpointStatus, parameters.fromExtension]);
// Enhanced TO options with endpoint availability // Enhanced TO options with endpoint availability
const enhancedToOptions = useMemo(() => { const enhancedToOptions = useMemo(() => {

View File

@ -93,15 +93,18 @@ export const useConvertParameters = (): ConvertParametersHook => {
if (!fromExtension || !toExtension) return false; if (!fromExtension || !toExtension) return false;
// Check if conversion is supported // Handle dynamic format identifiers (file-<extension>)
const supportedToExtensions = CONVERSION_MATRIX[fromExtension]; let supportedToExtensions: string[] = [];
if (!supportedToExtensions || !supportedToExtensions.includes(toExtension)) { if (fromExtension.startsWith('file-')) {
return false; // Dynamic format - use 'any' conversion options
supportedToExtensions = CONVERSION_MATRIX['any'] || [];
} else {
// Regular format - check conversion matrix
supportedToExtensions = CONVERSION_MATRIX[fromExtension] || [];
} }
// Additional validation for image conversions if (!supportedToExtensions.includes(toExtension)) {
if (['png', 'jpg'].includes(toExtension)) { return false;
return parameters.imageOptions.dpi >= 72 && parameters.imageOptions.dpi <= 600;
} }
return true; return true;
@ -123,6 +126,12 @@ export const useConvertParameters = (): ConvertParametersHook => {
} }
} }
// Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use file-to-pdf endpoint
return 'file-to-pdf';
}
return getEndpointNameUtil(fromExtension, toExtension); return getEndpointNameUtil(fromExtension, toExtension);
}; };
@ -142,12 +151,27 @@ export const useConvertParameters = (): ConvertParametersHook => {
} }
} }
// Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use file-to-pdf endpoint
return '/api/v1/convert/file/pdf';
}
return getEndpointUrl(fromExtension, toExtension); return getEndpointUrl(fromExtension, toExtension);
}; };
const getAvailableToExtensions = (fromExtension: string) => { const getAvailableToExtensions = (fromExtension: string) => {
if (!fromExtension) return []; if (!fromExtension) return [];
// Handle dynamic format identifiers (file-<extension>)
if (fromExtension.startsWith('file-')) {
// Dynamic format - use 'any' conversion options (file-to-pdf)
const supportedExtensions = CONVERSION_MATRIX['any'] || [];
return TO_FORMAT_OPTIONS.filter(option =>
supportedExtensions.includes(option.value)
);
}
let supportedExtensions = CONVERSION_MATRIX[fromExtension] || []; let supportedExtensions = CONVERSION_MATRIX[fromExtension] || [];
// If no explicit conversion exists, but file-to-pdf might be available, // If no explicit conversion exists, but file-to-pdf might be available,
@ -180,9 +204,13 @@ export const useConvertParameters = (): ConvertParametersHook => {
let fromExt = detectedExt; let fromExt = detectedExt;
let availableTargets = detectedExt ? CONVERSION_MATRIX[detectedExt] || [] : []; let availableTargets = detectedExt ? CONVERSION_MATRIX[detectedExt] || [] : [];
// If no explicit conversion exists for this file type, fall back to 'any' // If no explicit conversion exists for this file type, create a dynamic format entry
// which will attempt file-to-pdf conversion if available // and fall back to 'any' conversion logic for the actual endpoint
if (availableTargets.length === 0) { if (availableTargets.length === 0 && detectedExt) {
fromExt = `file-${detectedExt}`; // Create dynamic format identifier
availableTargets = CONVERSION_MATRIX['any'] || [];
} else if (availableTargets.length === 0) {
// No extension detected - fall back to 'any'
fromExt = 'any'; fromExt = 'any';
availableTargets = CONVERSION_MATRIX['any'] || []; availableTargets = CONVERSION_MATRIX['any'] || [];
} }

View File

@ -35,7 +35,25 @@ const toolDefinitions: Record<string, ToolDefinition> = {
maxFiles: -1, maxFiles: -1,
category: "manipulation", category: "manipulation",
description: "Change to and from PDF and other formats", description: "Change to and from PDF and other formats",
endpoints: ["pdf-to-img", "img-to-pdf", "pdf-to-word", "pdf-to-presentation", "pdf-to-text", "pdf-to-html", "pdf-to-xml", "html-to-pdf", "markdown-to-pdf", "file-to-pdf"] endpoints: ["pdf-to-img", "img-to-pdf", "pdf-to-word", "pdf-to-presentation", "pdf-to-text", "pdf-to-html", "pdf-to-xml", "html-to-pdf", "markdown-to-pdf", "file-to-pdf"],
supportedFormats: [
// Microsoft Office
"doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx",
// OpenDocument
"odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg",
// Text formats
"txt", "text", "xml", "rtf", "html", "lwp", "md",
// Images
"bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp",
// StarOffice
"sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw",
// Email formats
"eml",
// Archive formats
"zip",
// Other
"dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf"
]
}, },
swagger: { swagger: {
id: "swagger", id: "swagger",

View File

@ -197,6 +197,7 @@ function HomePageContent() {
files.forEach(addToActiveFiles); files.forEach(addToActiveFiles);
}} }}
accept={["*/*"]} accept={["*/*"]}
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
loading={false} loading={false}
showRecentFiles={true} showRecentFiles={true}
maxRecentFiles={8} maxRecentFiles={8}
@ -207,6 +208,7 @@ function HomePageContent() {
toolMode={!!selectedToolKey} toolMode={!!selectedToolKey}
showUpload={true} showUpload={true}
showBulkActions={!selectedToolKey} showBulkActions={!selectedToolKey}
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
{...(!selectedToolKey && { {...(!selectedToolKey && {
onOpenPageEditor: (file) => { onOpenPageEditor: (file) => {
handleViewChange("pageEditor"); handleViewChange("pageEditor");
@ -287,6 +289,7 @@ function HomePageContent() {
files.forEach(addToActiveFiles); files.forEach(addToActiveFiles);
}} }}
accept={["*/*"]} accept={["*/*"]}
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
loading={false} loading={false}
showRecentFiles={true} showRecentFiles={true}
maxRecentFiles={8} maxRecentFiles={8}