Replace filemanager with recent files system

This commit is contained in:
Reece 2025-06-27 21:32:20 +01:00
parent 61699a08a5
commit 35820e8a24
5 changed files with 406 additions and 58 deletions

View File

@ -0,0 +1,168 @@
import React, { useState } from "react";
import { Box, Flex, Group, Text, Button, TextInput, Select, Badge } from "@mantine/core";
import { useTranslation } from "react-i18next";
import SearchIcon from "@mui/icons-material/Search";
import SortIcon from "@mui/icons-material/Sort";
import FileCard from "../fileManagement/FileCard";
import { FileWithUrl } from "../../types/file";
interface FileGridProps {
files: FileWithUrl[];
onRemove?: (index: number) => void;
onDoubleClick?: (file: FileWithUrl) => void;
onView?: (file: FileWithUrl) => void;
onEdit?: (file: FileWithUrl) => void;
onSelect?: (fileId: string) => void;
selectedFiles?: string[];
showSearch?: boolean;
showSort?: boolean;
maxDisplay?: number; // If set, shows only this many files with "Show All" option
onShowAll?: () => void;
showingAll?: boolean;
}
type SortOption = 'date' | 'name' | 'size';
const FileGrid = ({
files,
onRemove,
onDoubleClick,
onView,
onEdit,
onSelect,
selectedFiles = [],
showSearch = false,
showSort = false,
maxDisplay,
onShowAll,
showingAll = false
}: FileGridProps) => {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState("");
const [sortBy, setSortBy] = useState<SortOption>('date');
// Filter files based on search term
const filteredFiles = files.filter(file =>
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);
case 'name':
return a.name.localeCompare(b.name);
case 'size':
return (b.size || 0) - (a.size || 0);
default:
return 0;
}
});
// Apply max display limit if specified
const displayFiles = maxDisplay && !showingAll
? sortedFiles.slice(0, maxDisplay)
: sortedFiles;
const hasMoreFiles = maxDisplay && !showingAll && sortedFiles.length > maxDisplay;
return (
<Box>
{/* Search and Sort Controls */}
{(showSearch || showSort) && (
<Group mb="md" justify="space-between" wrap="wrap" gap="sm">
{showSearch && (
<TextInput
placeholder={t("fileManager.searchFiles", "Search files...")}
leftSection={<SearchIcon size={16} />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.currentTarget.value)}
style={{ flexGrow: 1, maxWidth: 300, minWidth: 200 }}
/>
)}
{showSort && (
<Select
data={[
{ value: 'date', label: t("fileManager.sortByDate", "Sort by Date") },
{ value: 'name', label: t("fileManager.sortByName", "Sort by Name") },
{ value: 'size', label: t("fileManager.sortBySize", "Sort by Size") }
]}
value={sortBy}
onChange={(value) => setSortBy(value as SortOption)}
leftSection={<SortIcon size={16} />}
style={{ minWidth: 150 }}
/>
)}
</Group>
)}
{/* File Count Badge */}
{(showSearch || showSort) && (
<Group mb="sm">
<Badge variant="light" size="sm">
{displayFiles.length} {displayFiles.length === 1 ? 'file' : 'files'}
{hasMoreFiles && ` (${sortedFiles.length - maxDisplay!} more)`}
</Badge>
</Group>
)}
{/* Files Grid */}
<Flex
wrap="wrap"
gap="lg"
justify="flex-start"
style={{
width: "100%",
// Responsive grid spacing
'@media (max-width: 768px)': {
gap: 'md'
}
}}
>
{displayFiles.map((file, idx) => {
const originalIdx = files.findIndex(f => (f.id || f.name) === (file.id || file.name));
return (
<FileCard
key={file.id || file.name + idx}
file={file}
onRemove={onRemove ? () => onRemove(originalIdx) : undefined}
onDoubleClick={onDoubleClick ? () => onDoubleClick(file) : undefined}
onView={onView ? () => onView(file) : undefined}
onEdit={onEdit ? () => onEdit(file) : undefined}
isSelected={selectedFiles.includes(file.id || file.name)}
onSelect={onSelect ? () => onSelect(file.id || file.name) : undefined}
/>
);
})}
</Flex>
{/* Show All Button */}
{hasMoreFiles && onShowAll && (
<Group justify="center" mt="md">
<Button
variant="light"
onClick={onShowAll}
>
{t("fileManager.showAll", "Show All")} ({sortedFiles.length} files)
</Button>
</Group>
)}
{/* Empty State */}
{displayFiles.length === 0 && (
<Box style={{ textAlign: 'center', padding: '2rem' }}>
<Text c="dimmed">
{searchTerm
? t("fileManager.noFilesFound", "No files found matching your search")
: t("fileManager.noFiles", "No files available")
}
</Text>
</Box>
)}
</Box>
);
};
export default FileGrid;

View File

@ -1,9 +1,12 @@
import React, { useState, useCallback, useRef } from 'react';
import { Stack, Button, Text, Center } from '@mantine/core';
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Stack, Button, Text, Center, Box, Divider } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import { useTranslation } from 'react-i18next';
import FilePickerModal from './FilePickerModal';
import { fileStorage } from '../../services/fileStorage';
import { FileWithUrl } from '../../types/file';
import FileGrid from './FileGrid';
import MultiSelectControls from './MultiSelectControls';
interface FileUploadSelectorProps {
// Appearance
@ -20,6 +23,10 @@ interface FileUploadSelectorProps {
// Loading state
loading?: boolean;
disabled?: boolean;
// Recent files
showRecentFiles?: boolean;
maxRecentFiles?: number;
}
const FileUploadSelector = ({
@ -32,20 +39,40 @@ const FileUploadSelector = ({
accept = ["application/pdf"],
loading = false,
disabled = false,
showRecentFiles = true,
maxRecentFiles = 8,
}: FileUploadSelectorProps) => {
const { t } = useTranslation();
const [showFilePickerModal, setShowFilePickerModal] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = useCallback((uploadedFiles: File[]) => {
// Recent files state
const [recentFiles, setRecentFiles] = useState<FileWithUrl[]>([]);
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [showingAllRecent, setShowingAllRecent] = useState(false);
const [recentFilesLoading, setRecentFilesLoading] = useState(false);
const handleFileUpload = useCallback(async (uploadedFiles: File[]) => {
if (uploadedFiles.length === 0) return;
// Auto-save uploaded files to recent files
if (showRecentFiles) {
try {
for (const file of uploadedFiles) {
await fileStorage.storeFile(file);
}
// Refresh recent files list
loadRecentFiles();
} catch (error) {
console.error('Failed to save files to recent:', error);
}
}
if (onFilesSelect) {
onFilesSelect(uploadedFiles);
} else if (onFileSelect) {
onFileSelect(uploadedFiles[0]);
}
}, [onFileSelect, onFilesSelect]);
}, [onFileSelect, onFilesSelect, showRecentFiles]);
const handleFileInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
@ -64,23 +91,98 @@ const FileUploadSelector = ({
fileInputRef.current?.click();
}, []);
const handleStorageSelection = useCallback((selectedFiles: File[]) => {
if (selectedFiles.length === 0) return;
// Load recent files from storage
const loadRecentFiles = useCallback(async () => {
if (!showRecentFiles) return;
if (onFilesSelect) {
onFilesSelect(selectedFiles);
} else if (onFileSelect) {
onFileSelect(selectedFiles[0]);
setRecentFilesLoading(true);
try {
const files = await fileStorage.getAllFiles();
// Sort by last modified date (newest first)
const sortedFiles = files.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0));
setRecentFiles(sortedFiles);
} catch (error) {
console.error('Failed to load recent files:', error);
setRecentFiles([]);
} finally {
setRecentFilesLoading(false);
}
}, [showRecentFiles]);
// Convert FileWithUrl to File for upload
const convertToFile = async (fileWithUrl: FileWithUrl): Promise<File> => {
if (fileWithUrl.url && fileWithUrl.url.startsWith('blob:')) {
const response = await fetch(fileWithUrl.url);
const data = await response.arrayBuffer();
return new File([data], fileWithUrl.name, {
type: fileWithUrl.type || 'application/pdf',
lastModified: fileWithUrl.lastModified || Date.now()
});
}
// Load from IndexedDB
const storedFile = await fileStorage.getFile(fileWithUrl.id || fileWithUrl.name);
if (storedFile) {
return new File([storedFile.data], storedFile.name, {
type: storedFile.type,
lastModified: storedFile.lastModified
});
}
throw new Error('File not found in storage');
};
const handleRecentFileSelection = useCallback(async (file: FileWithUrl) => {
try {
const fileObj = await convertToFile(file);
if (onFilesSelect) {
onFilesSelect([fileObj]);
} else if (onFileSelect) {
onFileSelect(fileObj);
}
} catch (error) {
console.error('Failed to load file from recent:', error);
}
}, [onFileSelect, onFilesSelect]);
const handleSelectedRecentFiles = useCallback(async () => {
if (selectedFiles.length === 0) return;
try {
const selectedFileObjects = recentFiles.filter(f => selectedFiles.includes(f.id || f.name));
const filePromises = selectedFileObjects.map(convertToFile);
const files = await Promise.all(filePromises);
if (onFilesSelect) {
onFilesSelect(files);
}
setSelectedFiles([]);
} catch (error) {
console.error('Failed to load selected files:', error);
}
}, [selectedFiles, recentFiles, onFilesSelect]);
const toggleFileSelection = useCallback((fileId: string) => {
setSelectedFiles(prev =>
prev.includes(fileId)
? prev.filter(id => id !== fileId)
: [...prev, fileId]
);
}, []);
// Load recent files on mount
useEffect(() => {
loadRecentFiles();
}, [loadRecentFiles]);
// Get default title and subtitle from translations if not provided
const displayTitle = title || t("fileUpload.selectFiles", "Select files");
const displaySubtitle = subtitle || t("fileUpload.chooseFromStorageMultiple", "Choose files from storage or upload new PDFs");
return (
<>
<Stack align="center" gap="xl">
<Stack align="center" gap="sm">
{/* Title and description */}
<Stack align="center" gap="md">
<UploadFileIcon style={{ fontSize: 64 }} />
@ -94,19 +196,6 @@ const FileUploadSelector = ({
{/* Action buttons */}
<Stack align="center" gap="md" w="100%">
<Button
variant="filled"
size="lg"
onClick={() => setShowFilePickerModal(true)}
disabled={disabled || sharedFiles.length === 0}
loading={loading}
>
{loading ? "Loading..." : `Load from Storage (${sharedFiles.length} files available)`}
</Button>
<Text size="md" c="dimmed">
{t("fileUpload.or", "or")}
</Text>
{showDropzone ? (
<Dropzone
@ -114,7 +203,7 @@ const FileUploadSelector = ({
accept={accept}
multiple={true}
disabled={disabled || loading}
style={{ width: '100%', minHeight: 120 }}
style={{ width: '100%', height: "5rem" }}
activateOnClick={true}
>
<Center>
@ -142,7 +231,7 @@ const FileUploadSelector = ({
>
{t("fileUpload.uploadFiles", "Upload Files")}
</Button>
{/* Manual file input as backup */}
<input
ref={fileInputRef}
@ -155,15 +244,45 @@ const FileUploadSelector = ({
</Stack>
)}
</Stack>
</Stack>
{/* File Picker Modal */}
<FilePickerModal
opened={showFilePickerModal}
onClose={() => setShowFilePickerModal(false)}
storedFiles={sharedFiles}
onSelectFiles={handleStorageSelection}
/>
{/* Recent Files Section */}
{showRecentFiles && recentFiles.length > 0 && (
<Box w="100%" >
<Divider my="md" />
<Text size="lg" fw={500} mb="md">
{t("fileUpload.recentFiles", "Recent Files")}
</Text>
<MultiSelectControls
selectedCount={selectedFiles.length}
onClearSelection={() => setSelectedFiles([])}
onAddToUpload={handleSelectedRecentFiles}
/>
<FileGrid
files={recentFiles}
onDoubleClick={handleRecentFileSelection}
onSelect={toggleFileSelection}
selectedFiles={selectedFiles}
maxDisplay={showingAllRecent ? undefined : maxRecentFiles}
onShowAll={() => setShowingAllRecent(true)}
showingAll={showingAllRecent}
showSearch={showingAllRecent}
showSort={showingAllRecent}
/>
{showingAllRecent && (
<Center mt="md">
<Button
variant="light"
onClick={() => setShowingAllRecent(false)}
>
{t("fileUpload.showLess", "Show Less")}
</Button>
</Center>
)}
</Box>
)}
</Stack>
</>
);
};

View File

@ -0,0 +1,76 @@
import React from "react";
import { Box, Group, Text, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
interface MultiSelectControlsProps {
selectedCount: number;
onClearSelection: () => void;
onOpenInFileEditor?: () => void;
onOpenInPageEditor?: () => void;
onAddToUpload?: () => void; // New action for recent files
}
const MultiSelectControls = ({
selectedCount,
onClearSelection,
onOpenInFileEditor,
onOpenInPageEditor,
onAddToUpload
}: MultiSelectControlsProps) => {
const { t } = useTranslation();
if (selectedCount === 0) return null;
return (
<Box mb="md" p="md" style={{ backgroundColor: 'var(--mantine-color-blue-0)', borderRadius: 8 }}>
<Group justify="space-between">
<Text size="sm">
{selectedCount} {t("fileManager.filesSelected", "files selected")}
</Text>
<Group>
<Button
size="xs"
variant="light"
onClick={onClearSelection}
>
{t("fileManager.clearSelection", "Clear Selection")}
</Button>
{onAddToUpload && (
<Button
size="xs"
color="green"
onClick={onAddToUpload}
>
{t("fileManager.addToUpload", "Add to Upload")}
</Button>
)}
{onOpenInFileEditor && (
<Button
size="xs"
color="orange"
onClick={onOpenInFileEditor}
disabled={selectedCount === 0}
>
{t("fileManager.openInFileEditor", "Open in File Editor")}
</Button>
)}
{onOpenInPageEditor && (
<Button
size="xs"
color="blue"
onClick={onOpenInPageEditor}
disabled={selectedCount === 0}
>
{t("fileManager.openInPageEditor", "Open in Page Editor")}
</Button>
)}
</Group>
</Group>
</Box>
);
};
export default MultiSelectControls;

View File

@ -8,7 +8,6 @@ import LightModeIcon from '@mui/icons-material/LightMode';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import VisibilityIcon from "@mui/icons-material/Visibility";
import EditNoteIcon from "@mui/icons-material/EditNote";
import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile";
import FolderIcon from "@mui/icons-material/Folder";
import { Group } from "@mantine/core";
@ -29,14 +28,6 @@ const VIEW_OPTIONS = [
),
value: "pageEditor",
},
{
label: (
<Group gap={4}>
<InsertDriveFileIcon fontSize="small" />
</Group>
),
value: "fileManager",
},
{
label: (
<Group gap={4}>

View File

@ -13,7 +13,6 @@ import rainbowStyles from '../styles/rainbow.module.css';
import ToolPicker from "../components/tools/ToolPicker";
import TopControls from "../components/shared/TopControls";
import FileManager from "../components/fileManagement/FileManager";
import FileEditor from "../components/pageEditor/FileEditor";
import PageEditor from "../components/pageEditor/PageEditor";
import PageEditorControls from "../components/pageEditor/PageEditorControls";
@ -40,7 +39,7 @@ type ToolRegistry = {
const baseToolRegistry = {
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "viewer" },
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "viewer" },
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "fileManager" },
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "pageEditor" },
};
export default function HomePage() {
@ -376,16 +375,7 @@ export default function HomePage() {
/>
{/* Main content area */}
<Box className="flex-1 min-h-0 margin-top-200 relative z-10">
{currentView === "fileManager" ? (
<FileManager
files={storedFiles}
setFiles={setStoredFiles}
setCurrentView={handleViewChange}
onOpenFileEditor={handleOpenFileEditor}
onOpenPageEditor={handleOpenPageEditor}
onLoadFileToActive={addToActiveFiles}
/>
) : (currentView != "fileManager") && !activeFiles[0] ? (
{!activeFiles[0] ? (
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<FileUploadSelector
title={currentView === "viewer"
@ -397,9 +387,13 @@ export default function HomePage() {
onFileSelect={(file) => {
addToActiveFiles(file);
}}
allowMultiple={false}
onFilesSelect={(files) => {
files.forEach(addToActiveFiles);
}}
accept={["application/pdf"]}
loading={false}
showRecentFiles={true}
maxRecentFiles={8}
/>
</Container>
) : currentView === "fileEditor" ? (