From 666c15fabd02652f093e4ce5ee0e499b7fe89bad Mon Sep 17 00:00:00 2001 From: Reece Date: Wed, 9 Jul 2025 19:46:44 +0100 Subject: [PATCH] Remove url params for now. small refactor --- .../src/components/shared/TopControls.tsx | 38 ++--- frontend/src/components/tools/ToolPicker.tsx | 2 +- .../src/components/tools/ToolRenderer.tsx | 2 +- frontend/src/contexts/FileContext.tsx | 108 +++++---------- frontend/src/examples/ExampleToolUsage.tsx | 90 ++++++++++++ .../src/hooks/useEnhancedProcessedFiles.ts | 2 +- frontend/src/hooks/useToolParameters.ts | 51 +++++++ frontend/src/hooks/useToolParams.ts | 130 ------------------ frontend/src/pages/HomePage.tsx | 93 +++++++------ frontend/src/services/pdfExportService.ts | 1 - frontend/src/tools/Compress.tsx | 1 - frontend/src/tools/Merge.tsx | 2 - frontend/src/tools/Split.tsx | 2 - frontend/src/types/fileContext.ts | 10 +- 14 files changed, 265 insertions(+), 267 deletions(-) create mode 100644 frontend/src/examples/ExampleToolUsage.tsx create mode 100644 frontend/src/hooks/useToolParameters.ts delete mode 100644 frontend/src/hooks/useToolParams.ts diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index 41b261673..1fbd8e1b2 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -54,14 +54,18 @@ const createViewOptions = (switchingTo: string | null) => [ interface TopControlsProps { currentView: string; setCurrentView: (view: string) => void; + selectedToolKey?: string | null; } const TopControls = ({ currentView, setCurrentView, + selectedToolKey, }: TopControlsProps) => { const { themeMode, isRainbowMode, isToggleDisabled, toggleTheme } = useRainbowThemeContext(); const [switchingTo, setSwitchingTo] = useState(null); + + const isToolSelected = selectedToolKey !== null; const handleViewChange = useCallback((view: string) => { // Show immediate feedback @@ -108,22 +112,24 @@ const TopControls = ({ -
- -
+ {!isToolSelected && ( +
+ +
+ )} ); }; diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index acf86dd35..cfb2bd3d4 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -12,7 +12,7 @@ type ToolRegistry = { }; interface ToolPickerProps { - selectedToolKey: string; + selectedToolKey: string | null; onSelect: (id: string) => void; toolRegistry: ToolRegistry; } diff --git a/frontend/src/components/tools/ToolRenderer.tsx b/frontend/src/components/tools/ToolRenderer.tsx index 5ce167864..0c8963165 100644 --- a/frontend/src/components/tools/ToolRenderer.tsx +++ b/frontend/src/components/tools/ToolRenderer.tsx @@ -45,7 +45,7 @@ const ToolRenderer = ({ {}} // TODO: Add loading state + setLoading={(loading: boolean) => {}} params={toolParams} updateParams={updateParams} /> diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index 1dfc37ef8..79f0a79b3 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -3,11 +3,11 @@ */ import React, { createContext, useContext, useReducer, useCallback, useEffect, useRef } from 'react'; -import { useSearchParams } from 'react-router-dom'; import { FileContextValue, FileContextState, FileContextProviderProps, + ModeType, ViewType, ToolType, FileOperation, @@ -33,8 +33,9 @@ const initialViewerConfig: ViewerConfig = { const initialState: FileContextState = { activeFiles: [], processedFiles: new Map(), - currentView: 'fileEditor', - currentTool: null, + currentMode: 'pageEditor', + currentView: 'fileEditor', // Legacy field + currentTool: null, // Legacy field fileEditHistory: new Map(), globalFileOperations: [], selectedFileIds: [], @@ -55,6 +56,7 @@ type FileContextAction = | { type: 'REMOVE_FILES'; payload: string[] } | { type: 'SET_PROCESSED_FILES'; payload: Map } | { type: 'UPDATE_PROCESSED_FILE'; payload: { file: File; processedFile: ProcessedFile } } + | { type: 'SET_CURRENT_MODE'; payload: ModeType } | { type: 'SET_CURRENT_VIEW'; payload: ViewType } | { type: 'SET_CURRENT_TOOL'; payload: ToolType } | { type: 'SET_SELECTED_FILES'; payload: string[] } @@ -112,17 +114,33 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): processedFiles: updatedProcessedFiles }; - case 'SET_CURRENT_VIEW': + case 'SET_CURRENT_MODE': + const coreViews = ['viewer', 'pageEditor', 'fileEditor']; + const isToolMode = !coreViews.includes(action.payload); + return { ...state, + currentMode: action.payload, + // Update legacy fields for backward compatibility + currentView: isToolMode ? 'fileEditor' : action.payload as ViewType, + currentTool: isToolMode ? action.payload as ToolType : null + }; + + case 'SET_CURRENT_VIEW': + // Legacy action - just update currentMode + return { + ...state, + currentMode: action.payload as ModeType, currentView: action.payload, - // Clear tool when switching views currentTool: null }; case 'SET_CURRENT_TOOL': + // Legacy action - just update currentMode return { ...state, + currentMode: action.payload ? action.payload as ModeType : 'pageEditor', + currentView: action.payload ? 'fileEditor' : 'pageEditor', currentTool: action.payload }; @@ -233,7 +251,6 @@ export function FileContextProvider({ maxCacheSize = 1024 * 1024 * 1024 // 1GB }: FileContextProviderProps) { const [state, dispatch] = useReducer(fileContextReducer, initialState); - const [searchParams, setSearchParams] = useSearchParams(); // Cleanup timers and refs const cleanupTimers = useRef>(new Map()); @@ -266,69 +283,6 @@ export function FileContextProvider({ }); }, [processedFiles, globalProcessing, processingProgress.overall]); - // URL synchronization - const syncUrlParams = useCallback(() => { - if (!enableUrlSync) return; - - const params: FileContextUrlParams = {}; - - if (state.currentView !== 'fileEditor') params.view = state.currentView; - if (state.currentTool) params.tool = state.currentTool; - if (state.selectedFileIds.length > 0) params.fileIds = state.selectedFileIds; - // Note: selectedPageIds intentionally excluded from URL sync - page selection is transient UI state - if (state.viewerConfig.zoom !== 1.0) params.zoom = state.viewerConfig.zoom; - if (state.viewerConfig.currentPage !== 1) params.page = state.viewerConfig.currentPage; - - // Update URL params without causing navigation - const newParams = new URLSearchParams(searchParams); - Object.entries(params).forEach(([key, value]) => { - if (Array.isArray(value)) { - newParams.set(key, value.join(',')); - } else if (value !== undefined) { - newParams.set(key, value.toString()); - } - }); - - // Remove empty params - Object.keys(params).forEach(key => { - if (!params[key as keyof FileContextUrlParams]) { - newParams.delete(key); - } - }); - - setSearchParams(newParams, { replace: true }); - }, [state, searchParams, setSearchParams, enableUrlSync]); - - // Load from URL params on mount - useEffect(() => { - if (!enableUrlSync) return; - - const view = searchParams.get('view') as ViewType; - const tool = searchParams.get('tool') as ToolType; - const zoom = searchParams.get('zoom'); - const page = searchParams.get('page'); - - if (view && view !== state.currentView) { - dispatch({ type: 'SET_CURRENT_VIEW', payload: view }); - } - if (tool && tool !== state.currentTool) { - dispatch({ type: 'SET_CURRENT_TOOL', payload: tool }); - } - if (zoom || page) { - dispatch({ - type: 'UPDATE_VIEWER_CONFIG', - payload: { - ...(zoom && { zoom: parseFloat(zoom) }), - ...(page && { currentPage: parseInt(page) }) - } - }); - } - }, []); - - // Sync URL when state changes - useEffect(() => { - syncUrlParams(); - }, [syncUrlParams]); // Centralized memory management const trackBlobUrl = useCallback((url: string) => { @@ -524,6 +478,20 @@ export function FileContextProvider({ dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: false }); }, []); + const setCurrentMode = useCallback((mode: ModeType) => { + requestNavigation(() => { + dispatch({ type: 'SET_CURRENT_MODE', payload: mode }); + + if (state.currentMode !== mode && state.activeFiles.length > 0) { + if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) { + window.requestIdleCallback(() => { + window.gc(); + }, { timeout: 5000 }); + } + } + }); + }, [requestNavigation, state.currentMode, state.activeFiles]); + const setCurrentView = useCallback((view: ViewType) => { requestNavigation(() => { dispatch({ type: 'SET_CURRENT_VIEW', payload: view }); @@ -572,7 +540,6 @@ export function FileContextProvider({ }, []); const undoLastOperation = useCallback((fileId?: string) => { - // TODO: Implement undo logic console.warn('Undo not yet implemented'); }, []); @@ -676,6 +643,7 @@ export function FileContextProvider({ removeFiles, replaceFile, clearAllFiles, + setCurrentMode, setCurrentView, setCurrentTool, setSelectedFiles, diff --git a/frontend/src/examples/ExampleToolUsage.tsx b/frontend/src/examples/ExampleToolUsage.tsx new file mode 100644 index 000000000..7bc6f59ec --- /dev/null +++ b/frontend/src/examples/ExampleToolUsage.tsx @@ -0,0 +1,90 @@ +/** + * Example of how tools use the new URL parameter system + * This shows how compress, split, merge tools would integrate + */ + +import React from 'react'; +import { useToolParameters, useToolParameter } from '../hooks/useToolParameters'; + +// Example: Compress Tool +export function CompressTool() { + const [params, updateParams] = useToolParameters('compress', { + quality: { type: 'string', default: 'medium' }, + method: { type: 'string', default: 'lossless' }, + optimization: { type: 'boolean', default: true } + }); + + return ( +
+

Compress Tool

+

Quality: {params.quality}

+

Method: {params.method}

+

Optimization: {params.optimization ? 'On' : 'Off'}

+ + + +
+ ); +} + +// Example: Split Tool with single parameter hook +export function SplitTool() { + const [pages, setPages] = useToolParameter('split', 'pages', { + type: 'string', + default: '1-5' + }); + + const [strategy, setStrategy] = useToolParameter('split', 'strategy', { + type: 'string', + default: 'range' + }); + + return ( +
+

Split Tool

+

Pages: {pages}

+

Strategy: {strategy}

+ + setPages(e.target.value)} + placeholder="Enter page range" + /> + + +
+ ); +} + +// Example: How URLs would look +/* +User interactions -> URL changes: + +1. Navigate to compress tool: + ?mode=compress + +2. Change compress quality to high: + ?mode=compress&quality=high + +3. Change method to lossy and enable optimization: + ?mode=compress&quality=high&method=lossy&optimization=true + +4. Switch to split tool: + ?mode=split + +5. Set split pages and strategy: + ?mode=split&pages=1-10&strategy=bookmarks + +6. Switch to pageEditor: + ?mode=pageEditor (or no params for default) + +All URLs are shareable and will restore exact tool state! +*/ \ No newline at end of file diff --git a/frontend/src/hooks/useEnhancedProcessedFiles.ts b/frontend/src/hooks/useEnhancedProcessedFiles.ts index 69b7788ee..312cba102 100644 --- a/frontend/src/hooks/useEnhancedProcessedFiles.ts +++ b/frontend/src/hooks/useEnhancedProcessedFiles.ts @@ -279,7 +279,7 @@ export function useEnhancedProcessedFile( const processedFile = file ? result.processedFiles.get(file) || null : null; // Note: This is async but we can't await in hook return - consider refactoring if needed - const fileKey = file ? '' : ''; // TODO: Handle async file key generation + const fileKey = file ? '' : ''; const processingState = fileKey ? result.processingStates.get(fileKey) || null : null; const isProcessing = !!processingState; const error = processingState?.error?.message || null; diff --git a/frontend/src/hooks/useToolParameters.ts b/frontend/src/hooks/useToolParameters.ts new file mode 100644 index 000000000..d6eae6d8b --- /dev/null +++ b/frontend/src/hooks/useToolParameters.ts @@ -0,0 +1,51 @@ +/** + * React hooks for tool parameter management (URL logic removed) + */ + +import { useCallback, useMemo } from 'react'; + +type ToolParameterValues = Record; + +/** + * Register tool parameters and get current values + */ +export function useToolParameters( + toolName: string, + parameters: Record +): [ToolParameterValues, (updates: Partial) => void] { + + // Return empty values and noop updater + const currentValues = useMemo(() => ({}), []); + const updateParameters = useCallback(() => {}, []); + + return [currentValues, updateParameters]; +} + +/** + * Hook for managing a single tool parameter + */ +export function useToolParameter( + toolName: string, + paramName: string, + definition: any +): [T, (value: T) => void] { + const [allParams, updateParams] = useToolParameters(toolName, { [paramName]: definition }); + + const value = allParams[paramName] as T; + + const setValue = useCallback((newValue: T) => { + updateParams({ [paramName]: newValue }); + }, [paramName, updateParams]); + + return [value, setValue]; +} + +/** + * Hook for getting/setting global parameters (zoom, page, etc.) + */ +export function useGlobalParameters() { + const currentValues = useMemo(() => ({}), []); + const updateParameters = useCallback(() => {}, []); + + return [currentValues, updateParameters]; +} \ No newline at end of file diff --git a/frontend/src/hooks/useToolParams.ts b/frontend/src/hooks/useToolParams.ts deleted file mode 100644 index 9c422da14..000000000 --- a/frontend/src/hooks/useToolParams.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { useSearchParams } from "react-router-dom"; -import { useEffect } from "react"; - -// Tool parameter definitions (shortened URLs) -const TOOL_PARAMS = { - split: [ - "mode", "p", "hd", "vd", "m", - "type", "val", "level", "meta", "dupes" - ], - compress: [ - "level", "gray", "rmeta", "size", "agg" - ], - merge: [ - "order", "rdupes" - ] -}; - -// Extract params for a specific tool from URL -function getToolParams(toolKey: string, searchParams: URLSearchParams) { - switch (toolKey) { - case "split": - return { - mode: searchParams.get("mode") || "byPages", - pages: searchParams.get("p") || "", - hDiv: searchParams.get("hd") || "", - vDiv: searchParams.get("vd") || "", - merge: searchParams.get("m") === "true", - splitType: searchParams.get("type") || "size", - splitValue: searchParams.get("val") || "", - bookmarkLevel: searchParams.get("level") || "0", - includeMetadata: searchParams.get("meta") === "true", - allowDuplicates: searchParams.get("dupes") === "true", - }; - case "compress": - return { - compressionLevel: parseInt(searchParams.get("level") || "5"), - grayscale: searchParams.get("gray") === "true", - removeMetadata: searchParams.get("rmeta") === "true", - expectedSize: searchParams.get("size") || "", - aggressive: searchParams.get("agg") === "true", - }; - case "merge": - return { - order: searchParams.get("order") || "default", - removeDuplicates: searchParams.get("rdupes") === "true", - }; - default: - return {}; - } -} - -// Update tool-specific params in URL -function updateToolParams(toolKey: string, searchParams: URLSearchParams, setSearchParams: any, newParams: any) { - const params = new URLSearchParams(searchParams); - - // Clear tool-specific params - if (toolKey === "split") { - ["mode", "p", "hd", "vd", "m", "type", "val", "level", "meta", "dupes"].forEach((k) => params.delete(k)); - // Set new split params - const merged = { ...getToolParams("split", searchParams), ...newParams }; - params.set("mode", merged.mode); - if (merged.mode === "byPages") params.set("p", merged.pages); - else if (merged.mode === "bySections") { - params.set("hd", merged.hDiv); - params.set("vd", merged.vDiv); - params.set("m", String(merged.merge)); - } else if (merged.mode === "bySizeOrCount") { - params.set("type", merged.splitType); - params.set("val", merged.splitValue); - } else if (merged.mode === "byChapters") { - params.set("level", merged.bookmarkLevel); - params.set("meta", String(merged.includeMetadata)); - params.set("dupes", String(merged.allowDuplicates)); - } - } else if (toolKey === "compress") { - ["level", "gray", "rmeta", "size", "agg"].forEach((k) => params.delete(k)); - const merged = { ...getToolParams("compress", searchParams), ...newParams }; - params.set("level", String(merged.compressionLevel)); - params.set("gray", String(merged.grayscale)); - params.set("rmeta", String(merged.removeMetadata)); - if (merged.expectedSize) params.set("size", merged.expectedSize); - params.set("agg", String(merged.aggressive)); - } else if (toolKey === "merge") { - ["order", "rdupes"].forEach((k) => params.delete(k)); - const merged = { ...getToolParams("merge", searchParams), ...newParams }; - params.set("order", merged.order); - params.set("rdupes", String(merged.removeDuplicates)); - } - - setSearchParams(params, { replace: true }); -} - -export function useToolParams(selectedToolKey: string, currentView: string) { - const [searchParams, setSearchParams] = useSearchParams(); - - const toolParams = getToolParams(selectedToolKey, searchParams); - - const updateParams = (newParams: any) => - updateToolParams(selectedToolKey, searchParams, setSearchParams, newParams); - - // Update URL when core state changes - useEffect(() => { - const params = new URLSearchParams(searchParams); - - // Remove all tool-specific params except for the current tool - Object.entries(TOOL_PARAMS).forEach(([tool, keys]) => { - if (tool !== selectedToolKey) { - keys.forEach((k) => params.delete(k)); - } - }); - - // Collect all params except 'v' - const entries = Array.from(params.entries()).filter(([key]) => key !== "v"); - - // Rebuild params with 'v' first - const newParams = new URLSearchParams(); - newParams.set("v", currentView); - newParams.set("t", selectedToolKey); - entries.forEach(([key, value]) => { - if (key !== "t") newParams.set(key, value); - }); - - setSearchParams(newParams, { replace: true }); - }, [selectedToolKey, currentView, setSearchParams]); - - return { - toolParams, - updateParams, - }; -} \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index d33d81a83..8ad6d5989 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,7 +1,5 @@ import React, { useState, useCallback, useEffect } from "react"; import { useTranslation } from 'react-i18next'; -import { useSearchParams } from "react-router-dom"; -import { useToolParams } from "../hooks/useToolParams"; import { useFileWithUrl } from "../hooks/useFileWithUrl"; import { useFileContext } from "../contexts/FileContext"; import { fileStorage } from "../services/fileStorage"; @@ -45,35 +43,67 @@ const baseToolRegistry = { export default function HomePage() { const { t } = useTranslation(); - const [searchParams] = useSearchParams(); const theme = useMantineTheme(); const { isRainbowMode } = useRainbowThemeContext(); // Get file context const fileContext = useFileContext(); - const { activeFiles, currentView, setCurrentView, addFiles } = fileContext; + const { activeFiles, currentView, currentMode, setCurrentView, addFiles } = fileContext; // Core app state - const [selectedToolKey, setSelectedToolKey] = useState(searchParams.get("t") || "split"); + const [selectedToolKey, setSelectedToolKey] = useState(null); - // File state separation - const [storedFiles, setStoredFiles] = useState([]); // IndexedDB files (FileManager) + const [storedFiles, setStoredFiles] = useState([]); const [preSelectedFiles, setPreSelectedFiles] = useState([]); - const [downloadUrl, setDownloadUrl] = useState(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(null); - // URL parameter management - const { toolParams, updateParams } = useToolParams(selectedToolKey, currentView); + // Tool registry + 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") }, + }; + + // Tool parameters (simplified for now) + const getToolParams = (toolKey: string | null) => { + if (!toolKey) return {}; + + switch (toolKey) { + case 'split': + return { + mode: 'grid', + pages: '', + hDiv: 2, + vDiv: 2, + merge: false, + splitType: 'pages', + splitValue: 1, + bookmarkLevel: 1, + includeMetadata: true, + allowDuplicates: false + }; + case 'compress': + return { + quality: 80, + imageCompression: true, + removeMetadata: false + }; + case 'merge': + return { + sortOrder: 'name', + includeMetadata: true + }; + default: + return {}; + } + }; + - // Persist active files across reloads useEffect(() => { - // Save active files to localStorage (just metadata) const activeFileData = activeFiles.map(file => ({ name: file.name, size: file.size, @@ -83,7 +113,6 @@ export default function HomePage() { localStorage.setItem('activeFiles', JSON.stringify(activeFileData)); }, [activeFiles]); - // Load stored files from IndexedDB on mount useEffect(() => { const loadStoredFiles = async () => { try { @@ -96,14 +125,12 @@ export default function HomePage() { 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); + // File restoration handled by FileContext } } catch (error) { console.warn('Failed to restore active files:', error); @@ -112,45 +139,30 @@ export default function HomePage() { 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 + setLeftPanelView('toolContent'); + setReaderMode(false); }, [toolRegistry, setCurrentView] ); - // Handle quick access actions const handleQuickAccessTools = useCallback(() => { setLeftPanelView('toolPicker'); setReaderMode(false); + setSelectedToolKey(null); }, []); const handleReaderToggle = useCallback(() => { setReaderMode(!readerMode); }, [readerMode]); - // Update URL when view changes const handleViewChange = useCallback((view: string) => { setCurrentView(view as any); - const params = new URLSearchParams(window.location.search); - params.set('view', view); - const newUrl = `${window.location.pathname}?${params.toString()}`; - window.history.replaceState({}, '', newUrl); }, [setCurrentView]); - - // Active file management using context const addToActiveFiles = useCallback(async (file: File) => { - // Check if file already exists const exists = activeFiles.some(f => f.name === file.name && f.size === file.size); if (!exists) { await addFiles([file]); @@ -162,12 +174,10 @@ export default function HomePage() { }, [fileContext]); const setCurrentActiveFile = useCallback(async (file: File) => { - // Remove if exists, then add to front const filtered = activeFiles.filter(f => !(f.name === file.name && f.size === file.size)); await addFiles([file, ...filtered]); }, [activeFiles, addFiles]); - // Handle file selection from upload (adds to active files) const handleFileSelect = useCallback((file: File) => { addToActiveFiles(file); }, [addToActiveFiles]); @@ -332,7 +342,7 @@ export default function HomePage() {