mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-29 00:25:28 +00:00
Bug fixing, file management changes
This commit is contained in:
parent
3ebf75ae6f
commit
215bb86a8e
@ -27,9 +27,9 @@ interface FileItem {
|
|||||||
interface FileEditorProps {
|
interface FileEditorProps {
|
||||||
onOpenPageEditor?: (file: File) => void;
|
onOpenPageEditor?: (file: File) => void;
|
||||||
onMergeFiles?: (files: File[]) => void;
|
onMergeFiles?: (files: File[]) => void;
|
||||||
sharedFiles?: any[];
|
sharedFiles?: { file: File; url: string }[];
|
||||||
setSharedFiles?: (files: any[]) => void;
|
setSharedFiles?: (files: { file: File; url: string }[]) => void;
|
||||||
preSelectedFiles?: any[];
|
preSelectedFiles?: { file: File; url: string }[];
|
||||||
onClearPreSelection?: () => void;
|
onClearPreSelection?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ export interface PageEditorProps {
|
|||||||
setFile?: (file: { file: File; url: string } | null) => void;
|
setFile?: (file: { file: File; url: string } | null) => void;
|
||||||
downloadUrl?: string | null;
|
downloadUrl?: string | null;
|
||||||
setDownloadUrl?: (url: string | null) => void;
|
setDownloadUrl?: (url: string | null) => void;
|
||||||
|
sharedFiles?: { file: File; url: string }[];
|
||||||
|
|
||||||
// Optional callbacks to expose internal functions
|
// Optional callbacks to expose internal functions
|
||||||
onFunctionsReady?: (functions: {
|
onFunctionsReady?: (functions: {
|
||||||
|
@ -69,13 +69,6 @@ const PageThumbnail = ({
|
|||||||
}: PageThumbnailProps) => {
|
}: PageThumbnailProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={(el) => {
|
|
||||||
if (el) {
|
|
||||||
pageRefs.current.set(page.id, el);
|
|
||||||
} else {
|
|
||||||
pageRefs.current.delete(page.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
data-page-id={page.id}
|
data-page-id={page.id}
|
||||||
className={`
|
className={`
|
||||||
${styles.pageContainer}
|
${styles.pageContainer}
|
||||||
|
@ -22,6 +22,7 @@ interface FileManagerProps {
|
|||||||
allowMultiple?: boolean;
|
allowMultiple?: boolean;
|
||||||
setCurrentView?: (view: string) => void;
|
setCurrentView?: (view: string) => void;
|
||||||
onOpenFileEditor?: (selectedFiles?: FileWithUrl[]) => void;
|
onOpenFileEditor?: (selectedFiles?: FileWithUrl[]) => void;
|
||||||
|
onLoadFileToActive?: (file: File) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileManager = ({
|
const FileManager = ({
|
||||||
@ -30,6 +31,7 @@ const FileManager = ({
|
|||||||
allowMultiple = true,
|
allowMultiple = true,
|
||||||
setCurrentView,
|
setCurrentView,
|
||||||
onOpenFileEditor,
|
onOpenFileEditor,
|
||||||
|
onLoadFileToActive,
|
||||||
}: FileManagerProps) => {
|
}: FileManagerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -210,28 +212,56 @@ const FileManager = ({
|
|||||||
|
|
||||||
const handleFileDoubleClick = async (file: FileWithUrl) => {
|
const handleFileDoubleClick = async (file: FileWithUrl) => {
|
||||||
try {
|
try {
|
||||||
const url = await createBlobUrlForFile(file);
|
// Reconstruct File object from storage and add to active files
|
||||||
// Add file to the beginning of files array and switch to viewer
|
if (onLoadFileToActive) {
|
||||||
setFiles(prev => [{ file: file, url: url }, ...prev.filter(f => f.id !== file.id)]);
|
const reconstructedFile = await reconstructFileFromStorage(file);
|
||||||
setCurrentView && setCurrentView("viewer");
|
onLoadFileToActive(reconstructedFile);
|
||||||
|
setCurrentView && setCurrentView("viewer");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create blob URL for file:', error);
|
console.error('Failed to load file to active set:', error);
|
||||||
setNotification('Failed to open file. It may have been removed from storage.');
|
setNotification('Failed to open file. It may have been removed from storage.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileView = async (file: FileWithUrl) => {
|
const handleFileView = async (file: FileWithUrl) => {
|
||||||
try {
|
try {
|
||||||
const url = await createBlobUrlForFile(file);
|
// Reconstruct File object from storage and add to active files
|
||||||
// Add file to the beginning of files array and switch to viewer
|
if (onLoadFileToActive) {
|
||||||
setFiles(prev => [{ file: file, url: url }, ...prev.filter(f => f.id !== file.id)]);
|
const reconstructedFile = await reconstructFileFromStorage(file);
|
||||||
setCurrentView && setCurrentView("viewer");
|
onLoadFileToActive(reconstructedFile);
|
||||||
|
setCurrentView && setCurrentView("viewer");
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create blob URL for file:', error);
|
console.error('Failed to load file to active set:', error);
|
||||||
setNotification('Failed to open file. It may have been removed from storage.');
|
setNotification('Failed to open file. It may have been removed from storage.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reconstructFileFromStorage = async (fileWithUrl: FileWithUrl): Promise<File> => {
|
||||||
|
// If it's already a regular file, return it
|
||||||
|
if (fileWithUrl instanceof File) {
|
||||||
|
return fileWithUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct from IndexedDB
|
||||||
|
const arrayBuffer = await createBlobUrlForFile(fileWithUrl);
|
||||||
|
if (typeof arrayBuffer === 'string') {
|
||||||
|
// createBlobUrlForFile returned a blob URL, we need the actual data
|
||||||
|
const response = await fetch(arrayBuffer);
|
||||||
|
const data = await response.arrayBuffer();
|
||||||
|
return new File([data], fileWithUrl.name, {
|
||||||
|
type: fileWithUrl.type || 'application/pdf',
|
||||||
|
lastModified: fileWithUrl.lastModified || Date.now()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new File([arrayBuffer], fileWithUrl.name, {
|
||||||
|
type: fileWithUrl.type || 'application/pdf',
|
||||||
|
lastModified: fileWithUrl.lastModified || Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFileEdit = (file: FileWithUrl) => {
|
const handleFileEdit = (file: FileWithUrl) => {
|
||||||
if (onOpenFileEditor) {
|
if (onOpenFileEditor) {
|
||||||
onOpenFileEditor([file]);
|
onOpenFileEditor([file]);
|
||||||
@ -309,8 +339,11 @@ const FileManager = ({
|
|||||||
subtitle="Add files to your storage for easy access across tools"
|
subtitle="Add files to your storage for easy access across tools"
|
||||||
sharedFiles={[]} // FileManager is the source, so no shared files
|
sharedFiles={[]} // FileManager is the source, so no shared files
|
||||||
onFilesSelect={(uploadedFiles) => {
|
onFilesSelect={(uploadedFiles) => {
|
||||||
// Handle multiple files
|
// Handle multiple files - add to storage AND active set
|
||||||
handleDrop(uploadedFiles);
|
handleDrop(uploadedFiles);
|
||||||
|
if (onLoadFileToActive && uploadedFiles.length > 0) {
|
||||||
|
uploadedFiles.forEach(onLoadFileToActive);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
allowMultiple={allowMultiple}
|
allowMultiple={allowMultiple}
|
||||||
accept={["application/pdf"]}
|
accept={["application/pdf"]}
|
||||||
|
@ -10,14 +10,14 @@ interface FileUploadSelectorProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
showDropzone?: boolean;
|
showDropzone?: boolean;
|
||||||
|
|
||||||
// File handling
|
// File handling
|
||||||
sharedFiles?: any[];
|
sharedFiles?: any[];
|
||||||
onFileSelect: (file: File) => void;
|
onFileSelect: (file: File) => void;
|
||||||
onFilesSelect?: (files: File[]) => void;
|
onFilesSelect?: (files: File[]) => void;
|
||||||
allowMultiple?: boolean;
|
allowMultiple?: boolean;
|
||||||
accept?: string[];
|
accept?: string[];
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -40,7 +40,7 @@ const FileUploadSelector = ({
|
|||||||
|
|
||||||
const handleFileUpload = useCallback((uploadedFiles: File[]) => {
|
const handleFileUpload = useCallback((uploadedFiles: File[]) => {
|
||||||
if (uploadedFiles.length === 0) return;
|
if (uploadedFiles.length === 0) return;
|
||||||
|
|
||||||
if (allowMultiple && onFilesSelect) {
|
if (allowMultiple && onFilesSelect) {
|
||||||
onFilesSelect(uploadedFiles);
|
onFilesSelect(uploadedFiles);
|
||||||
} else {
|
} else {
|
||||||
@ -50,7 +50,7 @@ const FileUploadSelector = ({
|
|||||||
|
|
||||||
const handleStorageSelection = useCallback((selectedFiles: File[]) => {
|
const handleStorageSelection = useCallback((selectedFiles: File[]) => {
|
||||||
if (selectedFiles.length === 0) return;
|
if (selectedFiles.length === 0) return;
|
||||||
|
|
||||||
if (allowMultiple && onFilesSelect) {
|
if (allowMultiple && onFilesSelect) {
|
||||||
onFilesSelect(selectedFiles);
|
onFilesSelect(selectedFiles);
|
||||||
} else {
|
} else {
|
||||||
@ -81,13 +81,13 @@ const FileUploadSelector = ({
|
|||||||
disabled={disabled || sharedFiles.length === 0}
|
disabled={disabled || sharedFiles.length === 0}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
Load from Storage ({sharedFiles.length} files available)
|
{loading ? "Loading..." : `Load from Storage (${sharedFiles.length} files available)`}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Text size="md" c="dimmed">
|
<Text size="md" c="dimmed">
|
||||||
or
|
or
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{showDropzone ? (
|
{showDropzone ? (
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={handleFileUpload}
|
onDrop={handleFileUpload}
|
||||||
@ -115,8 +115,8 @@ const FileUploadSelector = ({
|
|||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
style={{ display: 'contents' }}
|
style={{ display: 'contents' }}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -139,4 +139,4 @@ const FileUploadSelector = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FileUploadSelector;
|
export default FileUploadSelector;
|
||||||
|
39
frontend/src/hooks/useFileWithUrl.ts
Normal file
39
frontend/src/hooks/useFileWithUrl.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to convert a File object to { file: File; url: string } format
|
||||||
|
* Creates blob URL on-demand and handles cleanup
|
||||||
|
*/
|
||||||
|
export function useFileWithUrl(file: File | null): { file: File; url: string } | null {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!file) return null;
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
// Return object with cleanup function
|
||||||
|
const result = { file, url };
|
||||||
|
|
||||||
|
// Store cleanup function for later use
|
||||||
|
(result as any)._cleanup = () => URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [file]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook variant that returns cleanup function separately
|
||||||
|
*/
|
||||||
|
export function useFileWithUrlAndCleanup(file: File | null): {
|
||||||
|
fileObj: { file: File; url: string } | null;
|
||||||
|
cleanup: () => void;
|
||||||
|
} {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!file) return { fileObj: null, cleanup: () => {} };
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
const fileObj = { file, url };
|
||||||
|
const cleanup = () => URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
return { fileObj, cleanup };
|
||||||
|
}, [file]);
|
||||||
|
}
|
@ -1,5 +1,17 @@
|
|||||||
import React, { useState, useCallback } from "react";
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
import { Box, Group, Container } from "@mantine/core";
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { useToolParams } from "../hooks/useToolParams";
|
||||||
|
import { useFileWithUrl } from "../hooks/useFileWithUrl";
|
||||||
|
import { fileStorage } from "../services/fileStorage";
|
||||||
|
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
||||||
|
import ContentCutIcon from "@mui/icons-material/ContentCut";
|
||||||
|
import ZoomInMapIcon from "@mui/icons-material/ZoomInMap";
|
||||||
|
import { Group, Paper, Box, Button, useMantineTheme, Container } from "@mantine/core";
|
||||||
|
import { useRainbowThemeContext } from "../components/shared/RainbowThemeProvider";
|
||||||
|
import rainbowStyles from '../styles/rainbow.module.css';
|
||||||
|
|
||||||
|
import ToolPicker from "../components/tools/ToolPicker";
|
||||||
import TopControls from "../components/shared/TopControls";
|
import TopControls from "../components/shared/TopControls";
|
||||||
import FileManager from "../components/fileManagement/FileManager";
|
import FileManager from "../components/fileManagement/FileManager";
|
||||||
import FileEditor from "../components/editor/FileEditor";
|
import FileEditor from "../components/editor/FileEditor";
|
||||||
@ -7,26 +19,169 @@ import PageEditor from "../components/editor/PageEditor";
|
|||||||
import PageEditorControls from "../components/editor/PageEditorControls";
|
import PageEditorControls from "../components/editor/PageEditorControls";
|
||||||
import Viewer from "../components/viewer/Viewer";
|
import Viewer from "../components/viewer/Viewer";
|
||||||
import FileUploadSelector from "../components/shared/FileUploadSelector";
|
import FileUploadSelector from "../components/shared/FileUploadSelector";
|
||||||
|
import SplitPdfPanel from "../tools/Split";
|
||||||
|
import CompressPdfPanel from "../tools/Compress";
|
||||||
|
import MergePdfPanel from "../tools/Merge";
|
||||||
|
import ToolRenderer from "../components/tools/ToolRenderer";
|
||||||
|
import QuickAccessBar from "../components/shared/QuickAccessBar";
|
||||||
|
|
||||||
|
type ToolRegistryEntry = {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
name: string;
|
||||||
|
component: React.ComponentType<any>;
|
||||||
|
view: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ToolRegistry = {
|
||||||
|
[key: string]: ToolRegistryEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Base tool registry without translations
|
||||||
|
const baseToolRegistry = {
|
||||||
|
split: { icon: <ContentCutIcon />, component: SplitPdfPanel, view: "viewer" },
|
||||||
|
compress: { icon: <ZoomInMapIcon />, component: CompressPdfPanel, view: "viewer" },
|
||||||
|
merge: { icon: <AddToPhotosIcon />, component: MergePdfPanel, view: "fileManager" },
|
||||||
|
};
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [files, setFiles] = useState([]); // Array of { file, url }
|
const { t } = useTranslation();
|
||||||
const [preSelectedFiles, setPreSelectedFiles] = useState([]);
|
const [searchParams] = useSearchParams();
|
||||||
const [currentView, setCurrentView] = useState("fileManager");
|
const theme = useMantineTheme();
|
||||||
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
const [downloadUrl, setDownloadUrl] = useState(null);
|
|
||||||
const [pageEditorFunctions, setPageEditorFunctions] = useState(null);
|
|
||||||
|
|
||||||
// Handle file selection from upload
|
// Core app state
|
||||||
const handleFileSelect = useCallback((file) => {
|
const [selectedToolKey, setSelectedToolKey] = useState<string>(searchParams.get("t") || "split");
|
||||||
const fileObj = { file, url: URL.createObjectURL(file) };
|
const [currentView, setCurrentView] = useState<string>(searchParams.get("v") || "viewer");
|
||||||
setFiles([fileObj]);
|
|
||||||
|
// File state separation
|
||||||
|
const [storedFiles, setStoredFiles] = useState<any[]>([]); // IndexedDB files (FileManager)
|
||||||
|
const [activeFiles, setActiveFiles] = useState<File[]>([]); // Active working set (persisted)
|
||||||
|
const [preSelectedFiles, setPreSelectedFiles] = useState([]);
|
||||||
|
|
||||||
|
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||||
|
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||||
|
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
||||||
|
const [readerMode, setReaderMode] = useState(false);
|
||||||
|
|
||||||
|
// Page editor functions
|
||||||
|
const [pageEditorFunctions, setPageEditorFunctions] = useState<any>(null);
|
||||||
|
|
||||||
|
// URL parameter management
|
||||||
|
const { toolParams, updateParams } = useToolParams(selectedToolKey, currentView);
|
||||||
|
|
||||||
|
// Persist active files across reloads
|
||||||
|
useEffect(() => {
|
||||||
|
// Save active files to localStorage (just metadata)
|
||||||
|
const activeFileData = activeFiles.map(file => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type,
|
||||||
|
lastModified: file.lastModified
|
||||||
|
}));
|
||||||
|
localStorage.setItem('activeFiles', JSON.stringify(activeFileData));
|
||||||
|
}, [activeFiles]);
|
||||||
|
|
||||||
|
// Load stored files from IndexedDB on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const loadStoredFiles = async () => {
|
||||||
|
try {
|
||||||
|
const files = await fileStorage.getAllFiles();
|
||||||
|
setStoredFiles(files);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load stored files:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadStoredFiles();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Restore active files on load
|
||||||
|
useEffect(() => {
|
||||||
|
const restoreActiveFiles = async () => {
|
||||||
|
try {
|
||||||
|
const savedFileData = JSON.parse(localStorage.getItem('activeFiles') || '[]');
|
||||||
|
if (savedFileData.length > 0) {
|
||||||
|
// TODO: Reconstruct files from IndexedDB when fileStorage is available
|
||||||
|
console.log('Would restore active files:', savedFileData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to restore active files:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
restoreActiveFiles();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toolRegistry: ToolRegistry = {
|
||||||
|
split: { ...baseToolRegistry.split, name: t("home.split.title", "Split PDF") },
|
||||||
|
compress: { ...baseToolRegistry.compress, name: t("home.compressPdfs.title", "Compress PDF") },
|
||||||
|
merge: { ...baseToolRegistry.merge, name: t("home.merge.title", "Merge PDFs") },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle tool selection
|
||||||
|
const handleToolSelect = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
setSelectedToolKey(id);
|
||||||
|
if (toolRegistry[id]?.view) setCurrentView(toolRegistry[id].view);
|
||||||
|
setLeftPanelView('toolContent'); // Switch to tool content view when a tool is selected
|
||||||
|
setReaderMode(false); // Exit reader mode when selecting a tool
|
||||||
|
},
|
||||||
|
[toolRegistry]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle quick access actions
|
||||||
|
const handleQuickAccessTools = useCallback(() => {
|
||||||
|
setLeftPanelView('toolPicker');
|
||||||
|
setReaderMode(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleReaderToggle = useCallback(() => {
|
||||||
|
setReaderMode(!readerMode);
|
||||||
|
}, [readerMode]);
|
||||||
|
|
||||||
|
// Update URL when view changes
|
||||||
|
const handleViewChange = useCallback((view: string) => {
|
||||||
|
setCurrentView(view);
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
params.set('view', view);
|
||||||
|
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Active file management
|
||||||
|
const addToActiveFiles = useCallback((file: File) => {
|
||||||
|
setActiveFiles(prev => {
|
||||||
|
// Avoid duplicates based on name and size
|
||||||
|
const exists = prev.some(f => f.name === file.name && f.size === file.size);
|
||||||
|
if (exists) return prev;
|
||||||
|
return [file, ...prev];
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeFromActiveFiles = useCallback((file: File) => {
|
||||||
|
setActiveFiles(prev => prev.filter(f => !(f.name === file.name && f.size === file.size)));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setCurrentActiveFile = useCallback((file: File) => {
|
||||||
|
setActiveFiles(prev => {
|
||||||
|
const filtered = prev.filter(f => !(f.name === file.name && f.size === file.size));
|
||||||
|
return [file, ...filtered];
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle file selection from upload (adds to active files)
|
||||||
|
const handleFileSelect = useCallback((file: File) => {
|
||||||
|
addToActiveFiles(file);
|
||||||
|
}, [addToActiveFiles]);
|
||||||
|
|
||||||
// Handle opening file editor with selected files
|
// Handle opening file editor with selected files
|
||||||
const handleOpenFileEditor = useCallback((selectedFiles) => {
|
const handleOpenFileEditor = useCallback((selectedFiles) => {
|
||||||
setPreSelectedFiles(selectedFiles || []);
|
setPreSelectedFiles(selectedFiles || []);
|
||||||
setCurrentView("fileEditor");
|
handleViewChange("fileEditor");
|
||||||
}, []);
|
}, [handleViewChange]);
|
||||||
|
|
||||||
|
const selectedTool = toolRegistry[selectedToolKey];
|
||||||
|
|
||||||
|
// Convert current active file to format expected by Viewer/PageEditor
|
||||||
|
const currentFileWithUrl = useFileWithUrl(activeFiles[0] || null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
@ -34,6 +189,86 @@ export default function HomePage() {
|
|||||||
gap={0}
|
gap={0}
|
||||||
className="min-h-screen w-screen overflow-hidden flex-nowrap flex"
|
className="min-h-screen w-screen overflow-hidden flex-nowrap flex"
|
||||||
>
|
>
|
||||||
|
{/* Quick Access Bar */}
|
||||||
|
<QuickAccessBar
|
||||||
|
onToolsClick={handleQuickAccessTools}
|
||||||
|
onReaderToggle={handleReaderToggle}
|
||||||
|
selectedToolKey={selectedToolKey}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
leftPanelView={leftPanelView}
|
||||||
|
readerMode={readerMode}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Left: Tool Picker OR Selected Tool Panel */}
|
||||||
|
<div
|
||||||
|
className={`h-screen z-sticky flex flex-col ${isRainbowMode ? rainbowStyles.rainbowPaper : ''} overflow-hidden`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--bg-surface)',
|
||||||
|
borderRight: '1px solid var(--border-subtle)',
|
||||||
|
width: sidebarsVisible && !readerMode ? '25vw' : '0px',
|
||||||
|
minWidth: sidebarsVisible && !readerMode ? '300px' : '0px',
|
||||||
|
maxWidth: sidebarsVisible && !readerMode ? '450px' : '0px',
|
||||||
|
transition: 'width 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), min-width 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), max-width 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
padding: sidebarsVisible && !readerMode ? '1rem' : '0rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
opacity: sidebarsVisible && !readerMode ? 1 : 0,
|
||||||
|
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{leftPanelView === 'toolPicker' ? (
|
||||||
|
// Tool Picker View
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
<ToolPicker
|
||||||
|
selectedToolKey={selectedToolKey}
|
||||||
|
onSelect={handleToolSelect}
|
||||||
|
toolRegistry={toolRegistry}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Selected Tool Content View
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* Back button */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<Button
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setLeftPanelView('toolPicker')}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
← Back to Tools
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tool title */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">{selectedTool?.name}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tool content */}
|
||||||
|
<div className="flex-1 min-h-0">
|
||||||
|
<ToolRenderer
|
||||||
|
selectedToolKey={selectedToolKey}
|
||||||
|
selectedTool={selectedTool}
|
||||||
|
pdfFile={activeFiles[0] || null}
|
||||||
|
files={activeFiles}
|
||||||
|
downloadUrl={downloadUrl}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
toolParams={toolParams}
|
||||||
|
updateParams={updateParams}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main View */}
|
||||||
<Box
|
<Box
|
||||||
className="flex-1 h-screen min-w-80 relative flex flex-col"
|
className="flex-1 h-screen min-w-80 relative flex flex-col"
|
||||||
style={{
|
style={{
|
||||||
@ -43,89 +278,105 @@ export default function HomePage() {
|
|||||||
{/* Top Controls */}
|
{/* Top Controls */}
|
||||||
<TopControls
|
<TopControls
|
||||||
currentView={currentView}
|
currentView={currentView}
|
||||||
setCurrentView={setCurrentView}
|
setCurrentView={handleViewChange}
|
||||||
/>
|
/>
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<Box className="flex-1 min-h-0 margin-top-200 relative z-10">
|
<Box className="flex-1 min-h-0 margin-top-200 relative z-10">
|
||||||
{currentView === "fileManager" ? (
|
{currentView === "fileManager" ? (
|
||||||
<FileManager
|
<FileManager
|
||||||
files={files}
|
files={storedFiles}
|
||||||
setFiles={setFiles}
|
setFiles={setStoredFiles}
|
||||||
setCurrentView={setCurrentView}
|
setCurrentView={handleViewChange}
|
||||||
onOpenFileEditor={handleOpenFileEditor}
|
onOpenFileEditor={handleOpenFileEditor}
|
||||||
/>
|
onLoadFileToActive={addToActiveFiles}
|
||||||
) : (currentView !== "fileManager") && !files[0] ? (
|
|
||||||
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
||||||
<FileUploadSelector
|
|
||||||
title={currentView === "viewer" ? "Select a PDF to view" : "Select a PDF to edit"}
|
|
||||||
subtitle="Choose a file from storage or upload a new PDF"
|
|
||||||
sharedFiles={files}
|
|
||||||
onFileSelect={handleFileSelect}
|
|
||||||
allowMultiple={false}
|
|
||||||
accept={["application/pdf"]}
|
|
||||||
loading={false}
|
|
||||||
/>
|
/>
|
||||||
</Container>
|
) : (currentView != "fileManager") && !activeFiles[0] ? (
|
||||||
) : currentView === "fileEditor" ? (
|
<Container size="lg" p="xl" h="100%" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<FileEditor
|
<FileUploadSelector
|
||||||
sharedFiles={files}
|
title={currentView === "viewer" ? "Select a PDF to view" : "Select a PDF to edit"}
|
||||||
setSharedFiles={setFiles}
|
subtitle="Choose a file from storage or upload a new PDF"
|
||||||
preSelectedFiles={preSelectedFiles}
|
sharedFiles={storedFiles}
|
||||||
onClearPreSelection={() => setPreSelectedFiles([])}
|
onFileSelect={(file) => {
|
||||||
onOpenPageEditor={(file) => {
|
addToActiveFiles(file);
|
||||||
const fileObj = { file, url: URL.createObjectURL(file) };
|
}}
|
||||||
setFiles([fileObj]);
|
allowMultiple={false}
|
||||||
setCurrentView("pageEditor");
|
accept={["application/pdf"]}
|
||||||
}}
|
loading={false}
|
||||||
onMergeFiles={(filesToMerge) => {
|
|
||||||
setFiles(filesToMerge.map(f => ({ file: f, url: URL.createObjectURL(f) })));
|
|
||||||
setCurrentView("viewer");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : currentView === "viewer" ? (
|
|
||||||
<Viewer
|
|
||||||
pdfFile={files[0]}
|
|
||||||
setPdfFile={(fileObj) => setFiles([fileObj])}
|
|
||||||
sidebarsVisible={sidebarsVisible}
|
|
||||||
setSidebarsVisible={setSidebarsVisible}
|
|
||||||
/>
|
|
||||||
) : currentView === "pageEditor" ? (
|
|
||||||
<>
|
|
||||||
<PageEditor
|
|
||||||
file={files[0]}
|
|
||||||
setFile={(fileObj) => setFiles([fileObj])}
|
|
||||||
downloadUrl={downloadUrl}
|
|
||||||
setDownloadUrl={setDownloadUrl}
|
|
||||||
onFunctionsReady={setPageEditorFunctions}
|
|
||||||
sharedFiles={files}
|
|
||||||
/>
|
|
||||||
{files[0] && pageEditorFunctions && (
|
|
||||||
<PageEditorControls
|
|
||||||
onClosePdf={pageEditorFunctions.closePdf}
|
|
||||||
onUndo={pageEditorFunctions.handleUndo}
|
|
||||||
onRedo={pageEditorFunctions.handleRedo}
|
|
||||||
canUndo={pageEditorFunctions.canUndo}
|
|
||||||
canRedo={pageEditorFunctions.canRedo}
|
|
||||||
onRotate={pageEditorFunctions.handleRotate}
|
|
||||||
onDelete={pageEditorFunctions.handleDelete}
|
|
||||||
onSplit={pageEditorFunctions.handleSplit}
|
|
||||||
onExportSelected={() => pageEditorFunctions.showExportPreview(true)}
|
|
||||||
onExportAll={() => pageEditorFunctions.showExportPreview(false)}
|
|
||||||
exportLoading={pageEditorFunctions.exportLoading}
|
|
||||||
selectionMode={pageEditorFunctions.selectionMode}
|
|
||||||
selectedPages={pageEditorFunctions.selectedPages}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Container>
|
||||||
</>
|
) : currentView === "fileEditor" ? (
|
||||||
) : (
|
<FileEditor
|
||||||
<FileManager
|
sharedFiles={activeFiles}
|
||||||
files={files}
|
setSharedFiles={setActiveFiles}
|
||||||
setFiles={setFiles}
|
preSelectedFiles={preSelectedFiles}
|
||||||
setCurrentView={setCurrentView}
|
onClearPreSelection={() => setPreSelectedFiles([])}
|
||||||
onOpenFileEditor={handleOpenFileEditor}
|
onOpenPageEditor={(file) => {
|
||||||
/>
|
setCurrentActiveFile(file);
|
||||||
)}
|
handleViewChange("pageEditor");
|
||||||
</Box>
|
}}
|
||||||
|
onMergeFiles={(filesToMerge) => {
|
||||||
|
// Add merged files to active set
|
||||||
|
filesToMerge.forEach(addToActiveFiles);
|
||||||
|
handleViewChange("viewer");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : currentView === "viewer" ? (
|
||||||
|
<Viewer
|
||||||
|
pdfFile={currentFileWithUrl}
|
||||||
|
setPdfFile={(fileObj) => {
|
||||||
|
if (fileObj) {
|
||||||
|
setCurrentActiveFile(fileObj.file);
|
||||||
|
} else {
|
||||||
|
setActiveFiles([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sidebarsVisible={sidebarsVisible}
|
||||||
|
setSidebarsVisible={setSidebarsVisible}
|
||||||
|
/>
|
||||||
|
) : currentView === "pageEditor" ? (
|
||||||
|
<>
|
||||||
|
<PageEditor
|
||||||
|
file={currentFileWithUrl}
|
||||||
|
setFile={(fileObj) => {
|
||||||
|
if (fileObj) {
|
||||||
|
setCurrentActiveFile(fileObj.file);
|
||||||
|
} else {
|
||||||
|
setActiveFiles([]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
downloadUrl={downloadUrl}
|
||||||
|
setDownloadUrl={setDownloadUrl}
|
||||||
|
onFunctionsReady={setPageEditorFunctions}
|
||||||
|
sharedFiles={activeFiles}
|
||||||
|
/>
|
||||||
|
{activeFiles[0] && pageEditorFunctions && (
|
||||||
|
<PageEditorControls
|
||||||
|
onClosePdf={pageEditorFunctions.closePdf}
|
||||||
|
onUndo={pageEditorFunctions.handleUndo}
|
||||||
|
onRedo={pageEditorFunctions.handleRedo}
|
||||||
|
canUndo={pageEditorFunctions.canUndo}
|
||||||
|
canRedo={pageEditorFunctions.canRedo}
|
||||||
|
onRotate={pageEditorFunctions.handleRotate}
|
||||||
|
onDelete={pageEditorFunctions.handleDelete}
|
||||||
|
onSplit={pageEditorFunctions.handleSplit}
|
||||||
|
onExportSelected={() => pageEditorFunctions.showExportPreview(true)}
|
||||||
|
onExportAll={() => pageEditorFunctions.showExportPreview(false)}
|
||||||
|
exportLoading={pageEditorFunctions.exportLoading}
|
||||||
|
selectionMode={pageEditorFunctions.selectionMode}
|
||||||
|
selectedPages={pageEditorFunctions.selectedPages}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<FileManager
|
||||||
|
files={storedFiles}
|
||||||
|
setFiles={setStoredFiles}
|
||||||
|
setCurrentView={handleViewChange}
|
||||||
|
onOpenFileEditor={handleOpenFileEditor}
|
||||||
|
onLoadFileToActive={addToActiveFiles}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user