{
- if (!isAnimating && draggedPage && page.pageNumber !== draggedPage && dropTarget === page.pageNumber) {
- return 'translateX(20px)';
- }
- return 'translateX(0)';
- })(),
transition: isAnimating ? 'none' : 'transform 0.2s ease-in-out'
}}
- draggable
- onDragStart={() => onDragStart(page.pageNumber)}
- onDragEnd={onDragEnd}
- onDragOver={onDragOver}
- onDragEnter={() => onDragEnter(page.pageNumber)}
- onDragLeave={onDragLeave}
- onDrop={(e) => onDrop(e, page.pageNumber)}
+ draggable={false}
>
{
{
- console.log('📸 Checkbox clicked for page', page.pageNumber);
e.stopPropagation();
onTogglePage(page.pageNumber);
}}
@@ -203,7 +240,7 @@ const PageThumbnail = React.memo(({
}
-
+
- ) : isLoadingThumbnail ? (
-
-
- Loading...
-
) : (
📄
@@ -407,30 +440,25 @@ const PageThumbnail = React.memo(({
)}
-
);
}, (prevProps, nextProps) => {
+ // Helper for shallow array comparison
+ const arraysEqual = (a: number[], b: number[]) => {
+ return a.length === b.length && a.every((val, i) => val === b[i]);
+ };
+
// Only re-render if essential props change
return (
prevProps.page.id === nextProps.page.id &&
prevProps.page.pageNumber === nextProps.page.pageNumber &&
prevProps.page.rotation === nextProps.page.rotation &&
prevProps.page.thumbnail === nextProps.page.thumbnail &&
- prevProps.selectedPages === nextProps.selectedPages && // Compare array reference - will re-render when selection changes
+ // Shallow compare selectedPages array for better stability
+ (prevProps.selectedPages === nextProps.selectedPages ||
+ arraysEqual(prevProps.selectedPages, nextProps.selectedPages)) &&
prevProps.selectionMode === nextProps.selectionMode &&
- prevProps.draggedPage === nextProps.draggedPage &&
- prevProps.dropTarget === nextProps.dropTarget &&
prevProps.movingPage === nextProps.movingPage &&
prevProps.isAnimating === nextProps.isAnimating
);
diff --git a/frontend/src/components/shared/FileCard.tsx b/frontend/src/components/shared/FileCard.tsx
index e4ff60eea..73ae01dba 100644
--- a/frontend/src/components/shared/FileCard.tsx
+++ b/frontend/src/components/shared/FileCard.tsx
@@ -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
{getFileDate(file)}
- {file.storedInIndexedDB && (
+ {record?.id && (
;
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('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 (
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}
diff --git a/frontend/src/components/shared/FilePickerModal.tsx b/frontend/src/components/shared/FilePickerModal.tsx
index f489c5a11..8aa054f25 100644
--- a/frontend/src/components/shared/FilePickerModal.tsx
+++ b/frontend/src/components/shared/FilePickerModal.tsx
@@ -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;
}
@@ -48,7 +48,7 @@ const FilePickerModal = ({
};
const selectAll = () => {
- setSelectedFileIds(storedFiles.map(f => f.id || f.name));
+ setSelectedFileIds(storedFiles.map(f => f.id).filter(Boolean));
};
const selectNone = () => {
@@ -57,7 +57,7 @@ const FilePickerModal = ({
const handleConfirm = async () => {
const selectedFiles = storedFiles.filter(f =>
- selectedFileIds.includes(f.id || f.name)
+ selectedFileIds.includes(f.id)
);
// Convert stored files to File objects
@@ -154,7 +154,7 @@ const FilePickerModal = ({
{storedFiles.map((file) => {
- const fileId = file.id || file.name;
+ const fileId = file.id;
const isSelected = selectedFileIds.includes(fileId);
return (
diff --git a/frontend/src/components/shared/FilePreview.tsx b/frontend/src/components/shared/FilePreview.tsx
index 4a58d4671..a13894040 100644
--- a/frontend/src/components/shared/FilePreview.tsx
+++ b/frontend/src/components/shared/FilePreview.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Box } from '@mantine/core';
-import { FileWithUrl } 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 | 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 | null) => void;
+ onFileClick?: (file: File | FileMetadata | null) => void;
onPrevious?: () => void;
onNext?: () => void;
}
diff --git a/frontend/src/components/shared/LandingPage.tsx b/frontend/src/components/shared/LandingPage.tsx
index 4af3d1202..6c1668a43 100644
--- a/frontend/src/components/shared/LandingPage.tsx
+++ b/frontend/src/components/shared/LandingPage.tsx
@@ -33,7 +33,7 @@ const LandingPage = () => {
{/* White PDF Page Background */}
{
ref={fileInputRef}
type="file"
multiple
- accept="*/*"
+ accept=".pdf,.zip"
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
diff --git a/frontend/src/components/shared/NavigationWarningModal.tsx b/frontend/src/components/shared/NavigationWarningModal.tsx
index a3d3983d2..c7f591c71 100644
--- a/frontend/src/components/shared/NavigationWarningModal.tsx
+++ b/frontend/src/components/shared/NavigationWarningModal.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
-import { useFileContext } from '../../contexts/FileContext';
+import { useNavigationGuard } from '../../contexts/NavigationContext';
interface NavigationWarningModalProps {
onApplyAndContinue?: () => Promise;
@@ -11,13 +11,13 @@ const NavigationWarningModal = ({
onApplyAndContinue,
onExportAndContinue
}: NavigationWarningModalProps) => {
- const {
- showNavigationWarning,
+ const {
+ showNavigationWarning,
hasUnsavedChanges,
- confirmNavigation,
cancelNavigation,
+ confirmNavigation,
setHasUnsavedChanges
- } = useFileContext();
+ } = useNavigationGuard();
const handleKeepWorking = () => {
cancelNavigation();
diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx
index bc041a923..80ef86c83 100644
--- a/frontend/src/components/shared/QuickAccessBar.tsx
+++ b/frontend/src/components/shared/QuickAccessBar.tsx
@@ -13,9 +13,9 @@ import { ButtonConfig } from '../../types/sidebar';
import './quickAccessBar/QuickAccessBar.css';
import AllToolsNavButton from './AllToolsNavButton';
import ActiveToolButton from "./quickAccessBar/ActiveToolButton";
-import {
- isNavButtonActive,
- getNavButtonStyle,
+import {
+ isNavButtonActive,
+ getNavButtonStyle,
getActiveNavButton,
} from './quickAccessBar/QuickAccessBar';
@@ -39,7 +39,7 @@ const QuickAccessBar = forwardRef(({
openFilesModal();
};
-
+
const buttonConfigs: ButtonConfig[] = [
{
id: 'read',
@@ -226,4 +226,4 @@ const QuickAccessBar = forwardRef(({
);
});
-export default QuickAccessBar;
\ No newline at end of file
+export default QuickAccessBar;
diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx
index e1fd78143..f3eaa0470 100644
--- a/frontend/src/components/shared/RightRail.tsx
+++ b/frontend/src/components/shared/RightRail.tsx
@@ -4,7 +4,8 @@ import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import './rightRail/RightRail.css';
import { useToolWorkflow } from '../../contexts/ToolWorkflowContext';
import { useRightRail } from '../../contexts/RightRailContext';
-import { useFileContext } from '../../contexts/FileContext';
+import { useFileState, useFileSelection, useFileManagement } from '../../contexts/FileContext';
+import { useNavigationState } from '../../contexts/NavigationContext';
import { useTranslation } from 'react-i18next';
import LanguageSelector from '../shared/LanguageSelector';
import { useRainbowThemeContext } from '../shared/RainbowThemeProvider';
@@ -23,17 +24,16 @@ export default function RightRail() {
// CSV input state for page selection
const [csvInput, setCsvInput] = useState("");
- // File/page selection handlers that adapt to current view
- const {
- currentView,
- activeFiles,
- processedFiles,
- selectedFileIds,
- selectedPageNumbers,
- setSelectedFiles,
- setSelectedPages,
- removeFiles
- } = useFileContext();
+ // Navigation view
+ const { currentMode: currentView } = useNavigationState();
+
+ // File state and selection
+ const { state, selectors } = useFileState();
+ const { selectedFiles, selectedFileIds, selectedPageNumbers, setSelectedFiles, setSelectedPages } = useFileSelection();
+ const { removeFiles } = useFileManagement();
+
+ const activeFiles = selectors.getFiles();
+ const fileRecords = selectors.getFileRecords();
// Compute selection state and total items
const getSelectionState = useCallback(() => {
@@ -45,48 +45,43 @@ export default function RightRail() {
if (currentView === 'pageEditor') {
let totalItems = 0;
- if (activeFiles.length === 1) {
- const pf = processedFiles.get(activeFiles[0]);
- totalItems = (pf?.totalPages as number) || (pf?.pages?.length || 0);
- } else if (activeFiles.length > 1) {
- activeFiles.forEach(file => {
- const pf = processedFiles.get(file);
- totalItems += (pf?.totalPages as number) || (pf?.pages?.length || 0);
- });
- }
- const selectedCount = selectedPageNumbers.length;
+ fileRecords.forEach(rec => {
+ const pf = rec.processedFile;
+ if (pf) {
+ totalItems += (pf.totalPages as number) || (pf.pages?.length || 0);
+ }
+ });
+ const selectedCount = Array.isArray(selectedPageNumbers) ? selectedPageNumbers.length : 0;
return { totalItems, selectedCount };
}
return { totalItems: 0, selectedCount: 0 };
- }, [currentView, activeFiles, processedFiles, selectedFileIds, selectedPageNumbers]);
+ }, [currentView, activeFiles, fileRecords, selectedFileIds, selectedPageNumbers]);
const { totalItems, selectedCount } = getSelectionState();
const handleSelectAll = useCallback(() => {
if (currentView === 'fileEditor' || currentView === 'viewer') {
- const allIds = activeFiles.map(f => (f as any).id || f.name);
+ // Select all file IDs
+ const allIds = state.files.ids;
setSelectedFiles(allIds);
return;
}
if (currentView === 'pageEditor') {
let totalPages = 0;
- if (activeFiles.length === 1) {
- const pf = processedFiles.get(activeFiles[0]);
- totalPages = (pf?.totalPages as number) || (pf?.pages?.length || 0);
- } else if (activeFiles.length > 1) {
- activeFiles.forEach(file => {
- const pf = processedFiles.get(file);
- totalPages += (pf?.totalPages as number) || (pf?.pages?.length || 0);
- });
- }
+ fileRecords.forEach(rec => {
+ const pf = rec.processedFile;
+ if (pf) {
+ totalPages += (pf.totalPages as number) || (pf.pages?.length || 0);
+ }
+ });
if (totalPages > 0) {
setSelectedPages(Array.from({ length: totalPages }, (_, i) => i + 1));
}
}
- }, [currentView, activeFiles, processedFiles, setSelectedFiles, setSelectedPages]);
+ }, [currentView, state.files.ids, fileRecords, setSelectedFiles, setSelectedPages]);
const handleDeselectAll = useCallback(() => {
if (currentView === 'fileEditor' || currentView === 'viewer') {
@@ -101,9 +96,7 @@ export default function RightRail() {
const handleExportAll = useCallback(() => {
if (currentView === 'fileEditor' || currentView === 'viewer') {
// Download selected files (or all if none selected)
- const filesToDownload = selectedCount > 0
- ? activeFiles.filter(f => selectedFileIds.includes((f as any).id || f.name))
- : activeFiles;
+ const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
filesToDownload.forEach(file => {
const link = document.createElement('a');
@@ -118,23 +111,18 @@ export default function RightRail() {
// Export all pages (not just selected)
pageEditorFunctions?.onExportAll?.();
}
- }, [currentView, selectedCount, activeFiles, selectedFileIds, pageEditorFunctions]);
+ }, [currentView, activeFiles, selectedFiles, pageEditorFunctions]);
const handleCloseSelected = useCallback(() => {
if (currentView !== 'fileEditor') return;
- if (selectedCount === 0) return;
-
- const fileIdsToClose = activeFiles.filter(f => selectedFileIds.includes((f as any).id || f.name))
- .map(f => (f as any).id || f.name);
-
- if (fileIdsToClose.length === 0) return;
+ if (selectedFileIds.length === 0) return;
// Close only selected files (do not delete from storage)
- removeFiles(fileIdsToClose, false);
+ removeFiles(selectedFileIds, false);
- // Update selection state to remove closed ids
- setSelectedFiles(selectedFileIds.filter(id => !fileIdsToClose.includes(id)));
- }, [currentView, selectedCount, activeFiles, selectedFileIds, removeFiles, setSelectedFiles]);
+ // Clear selection after closing
+ setSelectedFiles([]);
+ }, [currentView, selectedFileIds, removeFiles, setSelectedFiles]);
// CSV parsing functions for page selection
const parseCSVInput = useCallback((csv: string) => {
@@ -167,7 +155,9 @@ export default function RightRail() {
// Sync csvInput with selectedPageNumbers changes
useEffect(() => {
- const sortedPageNumbers = [...selectedPageNumbers].sort((a, b) => a - b);
+ const sortedPageNumbers = Array.isArray(selectedPageNumbers)
+ ? [...selectedPageNumbers].sort((a, b) => a - b)
+ : [];
const newCsvInput = sortedPageNumbers.join(', ');
setCsvInput(newCsvInput);
}, [selectedPageNumbers]);
@@ -285,7 +275,7 @@ export default function RightRail() {
@@ -307,7 +297,7 @@ export default function RightRail() {
radius="md"
className="right-rail-icon"
onClick={() => pageEditorFunctions?.handleDelete?.()}
- disabled={!pageControlsVisible || selectedCount === 0}
+ disabled={!pageControlsVisible || (Array.isArray(selectedPageNumbers) ? selectedPageNumbers.length === 0 : true)}
>