mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-25 13:47:39 +02:00
Files always get added to recent
This commit is contained in:
parent
45c775a8e0
commit
e378e27c60
@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useCallback, useEffect } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import { Modal } from '@mantine/core';
|
import { Modal } from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { FileWithUrl } from '../types/file';
|
import { FileWithUrl } from '../types/file';
|
||||||
import { useFileManager } from '../hooks/useFileManager';
|
import { useFileManager } from '../hooks/useFileManager';
|
||||||
import { useFilesModalContext } from '../contexts/FilesModalContext';
|
import { useFilesModalContext } from '../contexts/FilesModalContext';
|
||||||
@ -16,13 +15,12 @@ interface FileManagerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||||
const { t } = useTranslation();
|
const { isFilesModalOpen, closeFilesModal, onFilesSelect } = useFilesModalContext();
|
||||||
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
|
|
||||||
const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
|
const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, touchFile } = useFileManager();
|
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager();
|
||||||
|
|
||||||
// File management handlers
|
// File management handlers
|
||||||
const isFileSupported = useCallback((fileName: string) => {
|
const isFileSupported = useCallback((fileName: string) => {
|
||||||
@ -40,9 +38,6 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
try {
|
try {
|
||||||
const fileObjects = await Promise.all(
|
const fileObjects = await Promise.all(
|
||||||
files.map(async (fileWithUrl) => {
|
files.map(async (fileWithUrl) => {
|
||||||
if (fileWithUrl.file) {
|
|
||||||
return fileWithUrl.file;
|
|
||||||
}
|
|
||||||
return await convertToFile(fileWithUrl);
|
return await convertToFile(fileWithUrl);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -55,15 +50,14 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|||||||
const handleNewFileUpload = useCallback(async (files: File[]) => {
|
const handleNewFileUpload = useCallback(async (files: File[]) => {
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
try {
|
try {
|
||||||
// Store files and refresh recent files
|
// Files will get IDs assigned through onFilesSelect -> FileContext addFiles
|
||||||
await Promise.all(files.map(file => storeFile(file)));
|
|
||||||
onFilesSelect(files);
|
onFilesSelect(files);
|
||||||
await refreshRecentFiles();
|
await refreshRecentFiles();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to process dropped files:', error);
|
console.error('Failed to process dropped files:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [storeFile, onFilesSelect, refreshRecentFiles]);
|
}, [onFilesSelect, refreshRecentFiles]);
|
||||||
|
|
||||||
const handleRemoveFileByIndex = useCallback(async (index: number) => {
|
const handleRemoveFileByIndex = useCallback(async (index: number) => {
|
||||||
await handleRemoveFile(index, recentFiles, setRecentFiles);
|
await handleRemoveFile(index, recentFiles, setRecentFiles);
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Modal } from '@mantine/core';
|
|
||||||
import FileUploadSelector from './FileUploadSelector';
|
|
||||||
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
|
||||||
import { Tool } from '../../types/tool';
|
|
||||||
|
|
||||||
interface FileUploadModalProps {
|
|
||||||
selectedTool?: Tool | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FileUploadModal: React.FC<FileUploadModalProps> = ({ selectedTool }) => {
|
|
||||||
const { isFilesModalOpen, closeFilesModal, onFileSelect, onFilesSelect } = useFilesModalContext();
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
opened={isFilesModalOpen}
|
|
||||||
onClose={closeFilesModal}
|
|
||||||
title="Upload Files"
|
|
||||||
size="xl"
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
<FileUploadSelector
|
|
||||||
title="Upload Files"
|
|
||||||
subtitle="Choose files from storage or upload new files"
|
|
||||||
onFileSelect={onFileSelect}
|
|
||||||
onFilesSelect={onFilesSelect}
|
|
||||||
accept={["*/*"]}
|
|
||||||
supportedExtensions={selectedTool?.supportedFormats || ["pdf"]}
|
|
||||||
data-testid="file-upload-modal"
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileUploadModal;
|
|
@ -100,7 +100,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
case 'REMOVE_FILES':
|
case 'REMOVE_FILES':
|
||||||
const remainingFiles = state.activeFiles.filter(file => {
|
const remainingFiles = state.activeFiles.filter(file => {
|
||||||
const fileId = getFileId(file);
|
const fileId = getFileId(file);
|
||||||
return !action.payload.includes(fileId);
|
return !fileId || !action.payload.includes(fileId);
|
||||||
});
|
});
|
||||||
const safeSelectedFileIds = Array.isArray(state.selectedFileIds) ? state.selectedFileIds : [];
|
const safeSelectedFileIds = Array.isArray(state.selectedFileIds) ? state.selectedFileIds : [];
|
||||||
return {
|
return {
|
||||||
@ -491,26 +491,38 @@ export function FileContextProvider({
|
|||||||
}, [cleanupFile]);
|
}, [cleanupFile]);
|
||||||
|
|
||||||
// Action implementations
|
// Action implementations
|
||||||
const addFiles = useCallback(async (files: File[]) => {
|
const addFiles = useCallback(async (files: File[]): Promise<File[]> => {
|
||||||
dispatch({ type: 'ADD_FILES', payload: files });
|
dispatch({ type: 'ADD_FILES', payload: files });
|
||||||
|
|
||||||
// Auto-save to IndexedDB if persistence enabled
|
// Auto-save to IndexedDB if persistence enabled
|
||||||
if (enablePersistence) {
|
if (enablePersistence) {
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
try {
|
try {
|
||||||
// Check if file already has an ID (already in IndexedDB)
|
// Check if file already has an explicit ID property (already in IndexedDB)
|
||||||
const fileId = getFileId(file);
|
const fileId = getFileId(file);
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
// File doesn't have ID, store it and get the ID
|
// File doesn't have explicit ID, store it with thumbnail
|
||||||
const storedFile = await fileStorage.storeFile(file);
|
try {
|
||||||
|
// Generate thumbnail for better recent files experience
|
||||||
|
const thumbnail = await thumbnailGenerationService.generateThumbnail(file);
|
||||||
|
const storedFile = await fileStorage.storeFile(file, thumbnail);
|
||||||
// Add the ID to the file object
|
// Add the ID to the file object
|
||||||
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
||||||
|
} catch (thumbnailError) {
|
||||||
|
// If thumbnail generation fails, store without thumbnail
|
||||||
|
console.warn('Failed to generate thumbnail, storing without:', thumbnailError);
|
||||||
|
const storedFile = await fileStorage.storeFile(file);
|
||||||
|
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to store file:', error);
|
console.error('Failed to store file:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return files with their IDs assigned
|
||||||
|
return files;
|
||||||
}, [enablePersistence]);
|
}, [enablePersistence]);
|
||||||
|
|
||||||
const removeFiles = useCallback((fileIds: string[], deleteFromStorage: boolean = true) => {
|
const removeFiles = useCallback((fileIds: string[], deleteFromStorage: boolean = true) => {
|
||||||
@ -682,7 +694,7 @@ export function FileContextProvider({
|
|||||||
const getFileById = useCallback((fileId: string): File | undefined => {
|
const getFileById = useCallback((fileId: string): File | undefined => {
|
||||||
return state.activeFiles.find(file => {
|
return state.activeFiles.find(file => {
|
||||||
const actualFileId = getFileId(file);
|
const actualFileId = getFileId(file);
|
||||||
return actualFileId === fileId;
|
return actualFileId && actualFileId === fileId;
|
||||||
});
|
});
|
||||||
}, [state.activeFiles]);
|
}, [state.activeFiles]);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useRef, useCallback, useEffect } from 'react';
|
||||||
import { FileWithUrl } from '../types/file';
|
import { FileWithUrl } from '../types/file';
|
||||||
|
import { StoredFile } from '../services/fileStorage';
|
||||||
|
|
||||||
// Type for the context value - now contains everything directly
|
// Type for the context value - now contains everything directly
|
||||||
interface FileManagerContextValue {
|
interface FileManagerContextValue {
|
||||||
@ -40,7 +41,7 @@ interface FileManagerProviderProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onFileRemove: (index: number) => void;
|
onFileRemove: (index: number) => void;
|
||||||
modalHeight: string;
|
modalHeight: string;
|
||||||
storeFile: (file: File) => Promise<void>;
|
storeFile: (file: File) => Promise<StoredFile>;
|
||||||
refreshRecentFiles: () => Promise<void>;
|
refreshRecentFiles: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
const createdBlobUrls = useRef<Set<string>>(new Set());
|
const createdBlobUrls = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
// Computed values (with null safety)
|
// Computed values (with null safety)
|
||||||
const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id));
|
const selectedFiles = (recentFiles || []).filter(file => selectedFileIds.includes(file.id || file.name));
|
||||||
const filteredFiles = (recentFiles || []).filter(file =>
|
const filteredFiles = (recentFiles || []).filter(file =>
|
||||||
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
file.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
@ -122,14 +123,13 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
const files = Array.from(event.target.files || []);
|
const files = Array.from(event.target.files || []);
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
try {
|
try {
|
||||||
// Store files and refresh recent files (same as drag-and-drop)
|
// Create FileWithUrl objects - FileContext will handle storage and ID assignment
|
||||||
await Promise.all(files.map(file => storeFile(file)));
|
|
||||||
|
|
||||||
const fileWithUrls = files.map(file => {
|
const fileWithUrls = files.map(file => {
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
createdBlobUrls.current.add(url);
|
createdBlobUrls.current.add(url);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `local-${Date.now()}-${Math.random()}`,
|
// No ID assigned here - FileContext will handle storage and ID assignment
|
||||||
name: file.name,
|
name: file.name,
|
||||||
file,
|
file,
|
||||||
url,
|
url,
|
||||||
|
@ -1,21 +1,58 @@
|
|||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext, useState, useCallback } from 'react';
|
||||||
import { useFilesModal, UseFilesModalReturn } from '../hooks/useFilesModal';
|
|
||||||
import { useFileHandler } from '../hooks/useFileHandler';
|
import { useFileHandler } from '../hooks/useFileHandler';
|
||||||
|
|
||||||
interface FilesModalContextType extends UseFilesModalReturn {}
|
interface FilesModalContextType {
|
||||||
|
isFilesModalOpen: boolean;
|
||||||
|
openFilesModal: () => void;
|
||||||
|
closeFilesModal: () => void;
|
||||||
|
onFileSelect: (file: File) => void;
|
||||||
|
onFilesSelect: (files: File[]) => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
setOnModalClose: (callback: () => void) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const FilesModalContext = createContext<FilesModalContextType | null>(null);
|
const FilesModalContext = createContext<FilesModalContextType | null>(null);
|
||||||
|
|
||||||
export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const FilesModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const { addToActiveFiles, addMultipleFiles } = useFileHandler();
|
const { addToActiveFiles, addMultipleFiles } = useFileHandler();
|
||||||
|
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
||||||
|
const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>();
|
||||||
|
|
||||||
const filesModal = useFilesModal({
|
const openFilesModal = useCallback(() => {
|
||||||
onFileSelect: addToActiveFiles,
|
setIsFilesModalOpen(true);
|
||||||
onFilesSelect: addMultipleFiles,
|
}, []);
|
||||||
});
|
|
||||||
|
const closeFilesModal = useCallback(() => {
|
||||||
|
setIsFilesModalOpen(false);
|
||||||
|
onModalClose?.();
|
||||||
|
}, [onModalClose]);
|
||||||
|
|
||||||
|
const handleFileSelect = useCallback((file: File) => {
|
||||||
|
addToActiveFiles(file);
|
||||||
|
closeFilesModal();
|
||||||
|
}, [addToActiveFiles, closeFilesModal]);
|
||||||
|
|
||||||
|
const handleFilesSelect = useCallback((files: File[]) => {
|
||||||
|
addMultipleFiles(files);
|
||||||
|
closeFilesModal();
|
||||||
|
}, [addMultipleFiles, closeFilesModal]);
|
||||||
|
|
||||||
|
const setModalCloseCallback = useCallback((callback: () => void) => {
|
||||||
|
setOnModalClose(() => callback);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue: FilesModalContextType = {
|
||||||
|
isFilesModalOpen,
|
||||||
|
openFilesModal,
|
||||||
|
closeFilesModal,
|
||||||
|
onFileSelect: handleFileSelect,
|
||||||
|
onFilesSelect: handleFilesSelect,
|
||||||
|
onModalClose,
|
||||||
|
setOnModalClose: setModalCloseCallback,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilesModalContext.Provider value={filesModal}>
|
<FilesModalContext.Provider value={contextValue}>
|
||||||
{children}
|
{children}
|
||||||
</FilesModalContext.Provider>
|
</FilesModalContext.Provider>
|
||||||
);
|
);
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
import { useState, useCallback } from 'react';
|
|
||||||
|
|
||||||
export interface UseFilesModalReturn {
|
|
||||||
isFilesModalOpen: boolean;
|
|
||||||
openFilesModal: () => void;
|
|
||||||
closeFilesModal: () => void;
|
|
||||||
onFileSelect?: (file: File) => void;
|
|
||||||
onFilesSelect?: (files: File[]) => void;
|
|
||||||
onModalClose?: () => void;
|
|
||||||
setOnModalClose: (callback: () => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseFilesModalProps {
|
|
||||||
onFileSelect?: (file: File) => void;
|
|
||||||
onFilesSelect?: (files: File[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useFilesModal = ({
|
|
||||||
onFileSelect,
|
|
||||||
onFilesSelect
|
|
||||||
}: UseFilesModalProps = {}): UseFilesModalReturn => {
|
|
||||||
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
|
||||||
const [onModalClose, setOnModalClose] = useState<(() => void) | undefined>();
|
|
||||||
|
|
||||||
const openFilesModal = useCallback(() => {
|
|
||||||
setIsFilesModalOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeFilesModal = useCallback(() => {
|
|
||||||
setIsFilesModalOpen(false);
|
|
||||||
onModalClose?.();
|
|
||||||
}, [onModalClose]);
|
|
||||||
|
|
||||||
const handleFileSelect = useCallback((file: File) => {
|
|
||||||
onFileSelect?.(file);
|
|
||||||
closeFilesModal();
|
|
||||||
}, [onFileSelect, closeFilesModal]);
|
|
||||||
|
|
||||||
const handleFilesSelect = useCallback((files: File[]) => {
|
|
||||||
onFilesSelect?.(files);
|
|
||||||
closeFilesModal();
|
|
||||||
}, [onFilesSelect, closeFilesModal]);
|
|
||||||
|
|
||||||
const setModalCloseCallback = useCallback((callback: () => void) => {
|
|
||||||
setOnModalClose(() => callback);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFilesModalOpen,
|
|
||||||
openFilesModal,
|
|
||||||
closeFilesModal,
|
|
||||||
onFileSelect: handleFileSelect,
|
|
||||||
onFilesSelect: handleFilesSelect,
|
|
||||||
onModalClose,
|
|
||||||
setOnModalClose: setModalCloseCallback,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,8 +1,8 @@
|
|||||||
import { FileWithUrl } from "../types/file";
|
import { FileWithUrl } from "../types/file";
|
||||||
import { StoredFile, fileStorage } from "../services/fileStorage";
|
import { StoredFile, fileStorage } from "../services/fileStorage";
|
||||||
|
|
||||||
export function getFileId(file: File): string {
|
export function getFileId(file: File): string | null {
|
||||||
return (file as File & { id?: string }).id || file.name;
|
return (file as File & { id?: string }).id || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user