import React, { useState, useCallback, useRef, useMemo } from 'react'; import { Text, ActionIcon, CheckboxIndicator, Tooltip, Modal, Button, Group, Stack } from '@mantine/core'; import { useMediaQuery } from '@mantine/hooks'; import { alert } from '../toast'; import { useTranslation } from 'react-i18next'; import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined'; import CloseIcon from '@mui/icons-material/Close'; import VisibilityIcon from '@mui/icons-material/Visibility'; import UnarchiveIcon from '@mui/icons-material/Unarchive'; import PushPinIcon from '@mui/icons-material/PushPin'; import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { StirlingFileStub } from '../../types/fileContext'; import { zipFileService } from '../../services/zipFileService'; import styles from './FileEditor.module.css'; import { useFileContext } from '../../contexts/FileContext'; import { useFileState } from '../../contexts/file/fileHooks'; import { FileId } from '../../types/file'; import { formatFileSize } from '../../utils/fileUtils'; import ToolChain from '../shared/ToolChain'; import HoverActionMenu, { HoverAction } from '../shared/HoverActionMenu'; interface FileEditorThumbnailProps { file: StirlingFileStub; index: number; totalFiles: number; selectedFiles: FileId[]; selectionMode: boolean; onToggleFile: (fileId: FileId) => void; onCloseFile: (fileId: FileId) => void; onViewFile: (fileId: FileId) => void; _onSetStatus: (status: string) => void; onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void; onDownloadFile: (fileId: FileId) => void; onUnzipFile?: (fileId: FileId) => void; toolMode?: boolean; isSupported?: boolean; } const FileEditorThumbnail = ({ file, index, selectedFiles, onToggleFile, onCloseFile, onViewFile, _onSetStatus, onReorderFiles, onDownloadFile, onUnzipFile, isSupported = true, }: FileEditorThumbnailProps) => { const { t } = useTranslation(); const { pinFile, unpinFile, isFilePinned, activeFiles, actions: fileActions } = useFileContext(); const { state } = useFileState(); const hasError = state.ui.errorFileIds.includes(file.id); // ---- Drag state ---- const [isDragging, setIsDragging] = useState(false); const dragElementRef = useRef(null); const [showHoverMenu, setShowHoverMenu] = useState(false); const isMobile = useMediaQuery('(max-width: 1024px)'); const [showCloseModal, setShowCloseModal] = useState(false); // Resolve the actual File object for pin/unpin operations const actualFile = useMemo(() => { return activeFiles.find(f => f.fileId === file.id); }, [activeFiles, file.id]); const isPinned = actualFile ? isFilePinned(actualFile) : false; // Check if this is a ZIP file const isZipFile = zipFileService.isZipFileStub(file); const pageCount = file.processedFile?.totalPages || 0; const handleRef = useRef(null); // ---- Selection ---- const isSelected = selectedFiles.includes(file.id); // ---- Meta formatting ---- const prettySize = useMemo(() => { return formatFileSize(file.size); }, [file.size]); const extUpper = useMemo(() => { const m = /\.([a-z0-9]+)$/i.exec(file.name ?? ''); return (m?.[1] || '').toUpperCase(); }, [file.name]); const pageLabel = useMemo( () => pageCount > 0 ? `${pageCount} ${pageCount === 1 ? 'Page' : 'Pages'}` : '', [pageCount] ); const dateLabel = useMemo(() => { const d = new Date(file.lastModified); if (Number.isNaN(d.getTime())) return ''; return new Intl.DateTimeFormat(undefined, { month: 'short', day: '2-digit', year: 'numeric', }).format(d); }, [file.lastModified]); // ---- Drag & drop wiring ---- const fileElementRef = useCallback((element: HTMLDivElement | null) => { if (!element) return; dragElementRef.current = element; const dragCleanup = draggable({ element, getInitialData: () => ({ type: 'file', fileId: file.id, fileName: file.name, selectedFiles: [file.id] // Always drag only this file, ignore selection state }), onDragStart: () => { setIsDragging(true); }, onDrop: () => { setIsDragging(false); } }); const dropCleanup = dropTargetForElements({ element, getData: () => ({ type: 'file', fileId: file.id }), canDrop: ({ source }) => { const sourceData = source.data; return sourceData.type === 'file' && sourceData.fileId !== file.id; }, onDrop: ({ source }) => { const sourceData = source.data; if (sourceData.type === 'file' && onReorderFiles) { const sourceFileId = sourceData.fileId as FileId; const selectedFileIds = sourceData.selectedFiles as FileId[]; onReorderFiles(sourceFileId, file.id, selectedFileIds); } } }); return () => { dragCleanup(); dropCleanup(); }; }, [file.id, file.name, selectedFiles, onReorderFiles]); // Handle close with confirmation const handleCloseWithConfirmation = useCallback(() => { setShowCloseModal(true); }, []); const handleConfirmClose = useCallback(() => { onCloseFile(file.id); alert({ alertType: 'neutral', title: `Closed ${file.name}`, expandable: false, durationMs: 3500 }); setShowCloseModal(false); }, [file.id, file.name, onCloseFile]); const handleCancelClose = useCallback(() => { setShowCloseModal(false); }, []); // Build hover menu actions const hoverActions = useMemo(() => [ { id: 'view', icon: , label: t('openInViewer', 'Open in Viewer'), onClick: (e) => { e.stopPropagation(); onViewFile(file.id); }, }, { id: 'download', icon: , label: t('download', 'Download'), onClick: (e) => { e.stopPropagation(); onDownloadFile(file.id); alert({ alertType: 'success', title: `Downloading ${file.name}`, expandable: false, durationMs: 2500 }); }, }, { id: 'unzip', icon: , label: t('fileManager.unzip', 'Unzip'), onClick: (e) => { e.stopPropagation(); if (onUnzipFile) { onUnzipFile(file.id); alert({ alertType: 'success', title: `Unzipping ${file.name}`, expandable: false, durationMs: 2500 }); } }, hidden: !isZipFile || !onUnzipFile, }, { id: 'close', icon: , label: t('close', 'Close'), onClick: (e) => { e.stopPropagation(); handleCloseWithConfirmation(); }, color: 'red', } ], [t, file.id, file.name, isZipFile, onViewFile, onDownloadFile, onUnzipFile, handleCloseWithConfirmation]); // ---- Card interactions ---- const handleCardClick = () => { if (!isSupported) return; // Clear error state if file has an error (click to clear error) if (hasError) { try { fileActions.clearFileError(file.id); } catch (_e) { void _e; } } onToggleFile(file.id); }; const handleCardDoubleClick = () => { if (!isSupported) return; onViewFile(file.id); }; // ---- Style helpers ---- const getHeaderClassName = () => { if (hasError) return styles.headerError; if (!isSupported) return styles.headerUnsupported; return isSelected ? styles.headerSelected : styles.headerResting; }; return (
setShowHoverMenu(true)} onMouseLeave={() => setShowHoverMenu(false)} onDoubleClick={handleCardDoubleClick} > {/* Header bar */}
{/* Logo/checkbox area */}
{hasError ? (
{t('error._value', 'Error')}
) : isSupported ? ( onToggleFile(file.id)} color="var(--checkbox-checked-bg)" /> ) : (
{t('unsupported', 'Unsupported')}
)}
{/* Centered index */}
{index + 1}
{/* Action buttons group */}
{/* Pin/Unpin icon */} { e.stopPropagation(); if (actualFile) { if (isPinned) { unpinFile(actualFile); alert({ alertType: 'neutral', title: `Unpinned ${file.name}`, expandable: false, durationMs: 3000 }); } else { pinFile(actualFile); alert({ alertType: 'success', title: `Pinned ${file.name}`, expandable: false, durationMs: 3000 }); } } }} > {isPinned ? : }
{/* Title + meta line */}
{file.name} {/* e.g., v2 - Jan 29, 2025 - PDF file - 3 Pages */} {`v${file.versionNumber} - `} {dateLabel} {extUpper ? ` - ${extUpper} file` : ''} {pageLabel ? ` - ${pageLabel}` : ''}
{/* Preview area */}
{file.thumbnailUrl && ( {file.name} { const img = e.currentTarget; img.style.display = 'none'; img.parentElement?.setAttribute('data-thumb-missing', 'true'); }} style={{ maxWidth: '80%', maxHeight: '80%', objectFit: 'contain', borderRadius: 0, background: '#ffffff', border: '1px solid var(--border-default)', display: 'block', marginLeft: 'auto', marginRight: 'auto', alignSelf: 'start' }} /> )}
{/* Drag handle (span wrapper so we can attach a ref reliably) */} {/* Tool chain display at bottom */} {file.toolHistory && (
)}
{/* Hover Menu */} {/* Close Confirmation Modal */} {t('confirmCloseMessage', 'Are you sure you want to close this file?')} {file.name}
); }; export default React.memo(FileEditorThumbnail);