mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Remove url params for now. small refactor
This commit is contained in:
parent
33420c7e80
commit
666c15fabd
@ -54,15 +54,19 @@ const createViewOptions = (switchingTo: string | null) => [
|
|||||||
interface TopControlsProps {
|
interface TopControlsProps {
|
||||||
currentView: string;
|
currentView: string;
|
||||||
setCurrentView: (view: string) => void;
|
setCurrentView: (view: string) => void;
|
||||||
|
selectedToolKey?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TopControls = ({
|
const TopControls = ({
|
||||||
currentView,
|
currentView,
|
||||||
setCurrentView,
|
setCurrentView,
|
||||||
|
selectedToolKey,
|
||||||
}: TopControlsProps) => {
|
}: TopControlsProps) => {
|
||||||
const { themeMode, isRainbowMode, isToggleDisabled, toggleTheme } = useRainbowThemeContext();
|
const { themeMode, isRainbowMode, isToggleDisabled, toggleTheme } = useRainbowThemeContext();
|
||||||
const [switchingTo, setSwitchingTo] = useState<string | null>(null);
|
const [switchingTo, setSwitchingTo] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const isToolSelected = selectedToolKey !== null;
|
||||||
|
|
||||||
const handleViewChange = useCallback((view: string) => {
|
const handleViewChange = useCallback((view: string) => {
|
||||||
// Show immediate feedback
|
// Show immediate feedback
|
||||||
setSwitchingTo(view);
|
setSwitchingTo(view);
|
||||||
@ -108,6 +112,7 @@ const TopControls = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
</div>
|
</div>
|
||||||
|
{!isToolSelected && (
|
||||||
<div className="flex justify-center items-center h-full pointer-events-auto">
|
<div className="flex justify-center items-center h-full pointer-events-auto">
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={createViewOptions(switchingTo)}
|
data={createViewOptions(switchingTo)}
|
||||||
@ -124,6 +129,7 @@ const TopControls = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ type ToolRegistry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ToolPickerProps {
|
interface ToolPickerProps {
|
||||||
selectedToolKey: string;
|
selectedToolKey: string | null;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
toolRegistry: ToolRegistry;
|
toolRegistry: ToolRegistry;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ const ToolRenderer = ({
|
|||||||
<ToolComponent
|
<ToolComponent
|
||||||
files={files}
|
files={files}
|
||||||
setDownloadUrl={setDownloadUrl}
|
setDownloadUrl={setDownloadUrl}
|
||||||
setLoading={(loading: boolean) => {}} // TODO: Add loading state
|
setLoading={(loading: boolean) => {}}
|
||||||
params={toolParams}
|
params={toolParams}
|
||||||
updateParams={updateParams}
|
updateParams={updateParams}
|
||||||
/>
|
/>
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useContext, useReducer, useCallback, useEffect, useRef } from 'react';
|
import React, { createContext, useContext, useReducer, useCallback, useEffect, useRef } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
FileContextValue,
|
FileContextValue,
|
||||||
FileContextState,
|
FileContextState,
|
||||||
FileContextProviderProps,
|
FileContextProviderProps,
|
||||||
|
ModeType,
|
||||||
ViewType,
|
ViewType,
|
||||||
ToolType,
|
ToolType,
|
||||||
FileOperation,
|
FileOperation,
|
||||||
@ -33,8 +33,9 @@ const initialViewerConfig: ViewerConfig = {
|
|||||||
const initialState: FileContextState = {
|
const initialState: FileContextState = {
|
||||||
activeFiles: [],
|
activeFiles: [],
|
||||||
processedFiles: new Map(),
|
processedFiles: new Map(),
|
||||||
currentView: 'fileEditor',
|
currentMode: 'pageEditor',
|
||||||
currentTool: null,
|
currentView: 'fileEditor', // Legacy field
|
||||||
|
currentTool: null, // Legacy field
|
||||||
fileEditHistory: new Map(),
|
fileEditHistory: new Map(),
|
||||||
globalFileOperations: [],
|
globalFileOperations: [],
|
||||||
selectedFileIds: [],
|
selectedFileIds: [],
|
||||||
@ -55,6 +56,7 @@ type FileContextAction =
|
|||||||
| { type: 'REMOVE_FILES'; payload: string[] }
|
| { type: 'REMOVE_FILES'; payload: string[] }
|
||||||
| { type: 'SET_PROCESSED_FILES'; payload: Map<File, ProcessedFile> }
|
| { type: 'SET_PROCESSED_FILES'; payload: Map<File, ProcessedFile> }
|
||||||
| { type: 'UPDATE_PROCESSED_FILE'; payload: { file: File; processedFile: ProcessedFile } }
|
| { type: 'UPDATE_PROCESSED_FILE'; payload: { file: File; processedFile: ProcessedFile } }
|
||||||
|
| { type: 'SET_CURRENT_MODE'; payload: ModeType }
|
||||||
| { type: 'SET_CURRENT_VIEW'; payload: ViewType }
|
| { type: 'SET_CURRENT_VIEW'; payload: ViewType }
|
||||||
| { type: 'SET_CURRENT_TOOL'; payload: ToolType }
|
| { type: 'SET_CURRENT_TOOL'; payload: ToolType }
|
||||||
| { type: 'SET_SELECTED_FILES'; payload: string[] }
|
| { type: 'SET_SELECTED_FILES'; payload: string[] }
|
||||||
@ -112,17 +114,33 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
processedFiles: updatedProcessedFiles
|
processedFiles: updatedProcessedFiles
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'SET_CURRENT_VIEW':
|
case 'SET_CURRENT_MODE':
|
||||||
|
const coreViews = ['viewer', 'pageEditor', 'fileEditor'];
|
||||||
|
const isToolMode = !coreViews.includes(action.payload);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...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,
|
currentView: action.payload,
|
||||||
// Clear tool when switching views
|
|
||||||
currentTool: null
|
currentTool: null
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'SET_CURRENT_TOOL':
|
case 'SET_CURRENT_TOOL':
|
||||||
|
// Legacy action - just update currentMode
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
currentMode: action.payload ? action.payload as ModeType : 'pageEditor',
|
||||||
|
currentView: action.payload ? 'fileEditor' : 'pageEditor',
|
||||||
currentTool: action.payload
|
currentTool: action.payload
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -233,7 +251,6 @@ export function FileContextProvider({
|
|||||||
maxCacheSize = 1024 * 1024 * 1024 // 1GB
|
maxCacheSize = 1024 * 1024 * 1024 // 1GB
|
||||||
}: FileContextProviderProps) {
|
}: FileContextProviderProps) {
|
||||||
const [state, dispatch] = useReducer(fileContextReducer, initialState);
|
const [state, dispatch] = useReducer(fileContextReducer, initialState);
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
|
|
||||||
// Cleanup timers and refs
|
// Cleanup timers and refs
|
||||||
const cleanupTimers = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
const cleanupTimers = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||||
@ -266,69 +283,6 @@ export function FileContextProvider({
|
|||||||
});
|
});
|
||||||
}, [processedFiles, globalProcessing, processingProgress.overall]);
|
}, [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
|
// Centralized memory management
|
||||||
const trackBlobUrl = useCallback((url: string) => {
|
const trackBlobUrl = useCallback((url: string) => {
|
||||||
@ -524,6 +478,20 @@ export function FileContextProvider({
|
|||||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: false });
|
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) => {
|
const setCurrentView = useCallback((view: ViewType) => {
|
||||||
requestNavigation(() => {
|
requestNavigation(() => {
|
||||||
dispatch({ type: 'SET_CURRENT_VIEW', payload: view });
|
dispatch({ type: 'SET_CURRENT_VIEW', payload: view });
|
||||||
@ -572,7 +540,6 @@ export function FileContextProvider({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const undoLastOperation = useCallback((fileId?: string) => {
|
const undoLastOperation = useCallback((fileId?: string) => {
|
||||||
// TODO: Implement undo logic
|
|
||||||
console.warn('Undo not yet implemented');
|
console.warn('Undo not yet implemented');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -676,6 +643,7 @@ export function FileContextProvider({
|
|||||||
removeFiles,
|
removeFiles,
|
||||||
replaceFile,
|
replaceFile,
|
||||||
clearAllFiles,
|
clearAllFiles,
|
||||||
|
setCurrentMode,
|
||||||
setCurrentView,
|
setCurrentView,
|
||||||
setCurrentTool,
|
setCurrentTool,
|
||||||
setSelectedFiles,
|
setSelectedFiles,
|
||||||
|
90
frontend/src/examples/ExampleToolUsage.tsx
Normal file
90
frontend/src/examples/ExampleToolUsage.tsx
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
<h3>Compress Tool</h3>
|
||||||
|
<p>Quality: {params.quality}</p>
|
||||||
|
<p>Method: {params.method}</p>
|
||||||
|
<p>Optimization: {params.optimization ? 'On' : 'Off'}</p>
|
||||||
|
|
||||||
|
<button onClick={() => updateParams({ quality: 'high' })}>
|
||||||
|
Set High Quality
|
||||||
|
</button>
|
||||||
|
<button onClick={() => updateParams({ method: 'lossy' })}>
|
||||||
|
Set Lossy Method
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<div>
|
||||||
|
<h3>Split Tool</h3>
|
||||||
|
<p>Pages: {pages}</p>
|
||||||
|
<p>Strategy: {strategy}</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
value={pages}
|
||||||
|
onChange={(e) => setPages(e.target.value)}
|
||||||
|
placeholder="Enter page range"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<select value={strategy} onChange={(e) => setStrategy(e.target.value)}>
|
||||||
|
<option value="range">Range</option>
|
||||||
|
<option value="bookmarks">Bookmarks</option>
|
||||||
|
<option value="size">File Size</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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!
|
||||||
|
*/
|
@ -279,7 +279,7 @@ export function useEnhancedProcessedFile(
|
|||||||
|
|
||||||
const processedFile = file ? result.processedFiles.get(file) || null : null;
|
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
|
// 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 processingState = fileKey ? result.processingStates.get(fileKey) || null : null;
|
||||||
const isProcessing = !!processingState;
|
const isProcessing = !!processingState;
|
||||||
const error = processingState?.error?.message || null;
|
const error = processingState?.error?.message || null;
|
||||||
|
51
frontend/src/hooks/useToolParameters.ts
Normal file
51
frontend/src/hooks/useToolParameters.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* React hooks for tool parameter management (URL logic removed)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
type ToolParameterValues = Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register tool parameters and get current values
|
||||||
|
*/
|
||||||
|
export function useToolParameters(
|
||||||
|
toolName: string,
|
||||||
|
parameters: Record<string, any>
|
||||||
|
): [ToolParameterValues, (updates: Partial<ToolParameterValues>) => 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<T = any>(
|
||||||
|
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];
|
||||||
|
}
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,7 +1,5 @@
|
|||||||
import React, { useState, useCallback, useEffect } from "react";
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import { useToolParams } from "../hooks/useToolParams";
|
|
||||||
import { useFileWithUrl } from "../hooks/useFileWithUrl";
|
import { useFileWithUrl } from "../hooks/useFileWithUrl";
|
||||||
import { useFileContext } from "../contexts/FileContext";
|
import { useFileContext } from "../contexts/FileContext";
|
||||||
import { fileStorage } from "../services/fileStorage";
|
import { fileStorage } from "../services/fileStorage";
|
||||||
@ -45,35 +43,67 @@ const baseToolRegistry = {
|
|||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const { isRainbowMode } = useRainbowThemeContext();
|
const { isRainbowMode } = useRainbowThemeContext();
|
||||||
|
|
||||||
// Get file context
|
// Get file context
|
||||||
const fileContext = useFileContext();
|
const fileContext = useFileContext();
|
||||||
const { activeFiles, currentView, setCurrentView, addFiles } = fileContext;
|
const { activeFiles, currentView, currentMode, setCurrentView, addFiles } = fileContext;
|
||||||
|
|
||||||
// Core app state
|
// Core app state
|
||||||
const [selectedToolKey, setSelectedToolKey] = useState<string>(searchParams.get("t") || "split");
|
const [selectedToolKey, setSelectedToolKey] = useState<string | null>(null);
|
||||||
|
|
||||||
// File state separation
|
const [storedFiles, setStoredFiles] = useState<any[]>([]);
|
||||||
const [storedFiles, setStoredFiles] = useState<any[]>([]); // IndexedDB files (FileManager)
|
|
||||||
const [preSelectedFiles, setPreSelectedFiles] = useState([]);
|
const [preSelectedFiles, setPreSelectedFiles] = useState([]);
|
||||||
|
|
||||||
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
|
||||||
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||||
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
||||||
const [readerMode, setReaderMode] = useState(false);
|
const [readerMode, setReaderMode] = useState(false);
|
||||||
|
|
||||||
// Page editor functions
|
|
||||||
const [pageEditorFunctions, setPageEditorFunctions] = useState<any>(null);
|
const [pageEditorFunctions, setPageEditorFunctions] = useState<any>(null);
|
||||||
|
|
||||||
// URL parameter management
|
// Tool registry
|
||||||
const { toolParams, updateParams } = useToolParams(selectedToolKey, currentView);
|
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(() => {
|
useEffect(() => {
|
||||||
// Save active files to localStorage (just metadata)
|
|
||||||
const activeFileData = activeFiles.map(file => ({
|
const activeFileData = activeFiles.map(file => ({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
@ -83,7 +113,6 @@ export default function HomePage() {
|
|||||||
localStorage.setItem('activeFiles', JSON.stringify(activeFileData));
|
localStorage.setItem('activeFiles', JSON.stringify(activeFileData));
|
||||||
}, [activeFiles]);
|
}, [activeFiles]);
|
||||||
|
|
||||||
// Load stored files from IndexedDB on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadStoredFiles = async () => {
|
const loadStoredFiles = async () => {
|
||||||
try {
|
try {
|
||||||
@ -96,14 +125,12 @@ export default function HomePage() {
|
|||||||
loadStoredFiles();
|
loadStoredFiles();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Restore active files on load
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const restoreActiveFiles = async () => {
|
const restoreActiveFiles = async () => {
|
||||||
try {
|
try {
|
||||||
const savedFileData = JSON.parse(localStorage.getItem('activeFiles') || '[]');
|
const savedFileData = JSON.parse(localStorage.getItem('activeFiles') || '[]');
|
||||||
if (savedFileData.length > 0) {
|
if (savedFileData.length > 0) {
|
||||||
// TODO: Reconstruct files from IndexedDB when fileStorage is available
|
// File restoration handled by FileContext
|
||||||
console.log('Would restore active files:', savedFileData);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to restore active files:', error);
|
console.warn('Failed to restore active files:', error);
|
||||||
@ -112,45 +139,30 @@ export default function HomePage() {
|
|||||||
restoreActiveFiles();
|
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(
|
const handleToolSelect = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
setSelectedToolKey(id);
|
setSelectedToolKey(id);
|
||||||
if (toolRegistry[id]?.view) setCurrentView(toolRegistry[id].view);
|
if (toolRegistry[id]?.view) setCurrentView(toolRegistry[id].view);
|
||||||
setLeftPanelView('toolContent'); // Switch to tool content view when a tool is selected
|
setLeftPanelView('toolContent');
|
||||||
setReaderMode(false); // Exit reader mode when selecting a tool
|
setReaderMode(false);
|
||||||
},
|
},
|
||||||
[toolRegistry, setCurrentView]
|
[toolRegistry, setCurrentView]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle quick access actions
|
|
||||||
const handleQuickAccessTools = useCallback(() => {
|
const handleQuickAccessTools = useCallback(() => {
|
||||||
setLeftPanelView('toolPicker');
|
setLeftPanelView('toolPicker');
|
||||||
setReaderMode(false);
|
setReaderMode(false);
|
||||||
|
setSelectedToolKey(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleReaderToggle = useCallback(() => {
|
const handleReaderToggle = useCallback(() => {
|
||||||
setReaderMode(!readerMode);
|
setReaderMode(!readerMode);
|
||||||
}, [readerMode]);
|
}, [readerMode]);
|
||||||
|
|
||||||
// Update URL when view changes
|
|
||||||
const handleViewChange = useCallback((view: string) => {
|
const handleViewChange = useCallback((view: string) => {
|
||||||
setCurrentView(view as any);
|
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]);
|
}, [setCurrentView]);
|
||||||
|
|
||||||
// Active file management using context
|
|
||||||
const addToActiveFiles = useCallback(async (file: File) => {
|
const addToActiveFiles = useCallback(async (file: File) => {
|
||||||
// Check if file already exists
|
|
||||||
const exists = activeFiles.some(f => f.name === file.name && f.size === file.size);
|
const exists = activeFiles.some(f => f.name === file.name && f.size === file.size);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
await addFiles([file]);
|
await addFiles([file]);
|
||||||
@ -162,12 +174,10 @@ export default function HomePage() {
|
|||||||
}, [fileContext]);
|
}, [fileContext]);
|
||||||
|
|
||||||
const setCurrentActiveFile = useCallback(async (file: File) => {
|
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));
|
const filtered = activeFiles.filter(f => !(f.name === file.name && f.size === file.size));
|
||||||
await addFiles([file, ...filtered]);
|
await addFiles([file, ...filtered]);
|
||||||
}, [activeFiles, addFiles]);
|
}, [activeFiles, addFiles]);
|
||||||
|
|
||||||
// Handle file selection from upload (adds to active files)
|
|
||||||
const handleFileSelect = useCallback((file: File) => {
|
const handleFileSelect = useCallback((file: File) => {
|
||||||
addToActiveFiles(file);
|
addToActiveFiles(file);
|
||||||
}, [addToActiveFiles]);
|
}, [addToActiveFiles]);
|
||||||
@ -332,7 +342,7 @@ export default function HomePage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setLeftPanelView('toolPicker')}
|
onClick={handleQuickAccessTools}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
← {t("fileUpload.backToTools", "Back to Tools")}
|
← {t("fileUpload.backToTools", "Back to Tools")}
|
||||||
@ -353,8 +363,8 @@ export default function HomePage() {
|
|||||||
files={activeFiles}
|
files={activeFiles}
|
||||||
downloadUrl={downloadUrl}
|
downloadUrl={downloadUrl}
|
||||||
setDownloadUrl={setDownloadUrl}
|
setDownloadUrl={setDownloadUrl}
|
||||||
toolParams={toolParams}
|
toolParams={getToolParams(selectedToolKey)}
|
||||||
updateParams={updateParams}
|
updateParams={() => {}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -373,6 +383,7 @@ export default function HomePage() {
|
|||||||
<TopControls
|
<TopControls
|
||||||
currentView={currentView}
|
currentView={currentView}
|
||||||
setCurrentView={handleViewChange}
|
setCurrentView={handleViewChange}
|
||||||
|
selectedToolKey={selectedToolKey}
|
||||||
/>
|
/>
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<Box
|
<Box
|
||||||
|
@ -186,7 +186,6 @@ export class PDFExportService {
|
|||||||
*/
|
*/
|
||||||
async downloadAsZip(blobs: Blob[], filenames: string[], zipFilename: string): Promise<void> {
|
async downloadAsZip(blobs: Blob[], filenames: string[], zipFilename: string): Promise<void> {
|
||||||
// For now, download files individually
|
// For now, download files individually
|
||||||
// TODO: Implement ZIP creation when needed
|
|
||||||
blobs.forEach((blob, index) => {
|
blobs.forEach((blob, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.downloadFile(blob, filenames[index]);
|
this.downloadFile(blob, filenames[index]);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
|
import { Stack, Slider, Group, Text, Button, Checkbox, TextInput, Paper } from "@mantine/core";
|
||||||
import { FileWithUrl } from "../types/file";
|
import { FileWithUrl } from "../types/file";
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core";
|
import { Paper, Button, Checkbox, Stack, Text, Group, Loader, Alert } from "@mantine/core";
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FileWithUrl } from "../types/file";
|
import { FileWithUrl } from "../types/file";
|
||||||
import { fileStorage } from "../services/fileStorage";
|
import { fileStorage } from "../services/fileStorage";
|
||||||
@ -22,7 +21,6 @@ const MergePdfPanel: React.FC<MergePdfPanelProps> = ({
|
|||||||
updateParams,
|
updateParams,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
|
const [selectedFiles, setSelectedFiles] = useState<boolean[]>([]);
|
||||||
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
const [downloadUrl, setLocalDownloadUrl] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Paper,
|
Paper,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useSearchParams } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import { FileWithUrl } from "../types/file";
|
import { FileWithUrl } from "../types/file";
|
||||||
@ -42,7 +41,6 @@ const SplitPdfPanel: React.FC<SplitPdfPanelProps> = ({
|
|||||||
updateParams,
|
updateParams,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const [status, setStatus] = useState("");
|
const [status, setStatus] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
@ -5,8 +5,10 @@
|
|||||||
import { ProcessedFile } from './processing';
|
import { ProcessedFile } from './processing';
|
||||||
import { PDFDocument, PDFPage, PageOperation } from './pageEditor';
|
import { PDFDocument, PDFPage, PageOperation } from './pageEditor';
|
||||||
|
|
||||||
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress';
|
||||||
|
|
||||||
|
// Legacy types for backward compatibility during transition
|
||||||
|
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
||||||
export type ToolType = 'merge' | 'split' | 'compress' | null;
|
export type ToolType = 'merge' | 'split' | 'compress' | null;
|
||||||
|
|
||||||
export interface FileOperation {
|
export interface FileOperation {
|
||||||
@ -36,6 +38,8 @@ export interface FileContextState {
|
|||||||
processedFiles: Map<File, ProcessedFile>;
|
processedFiles: Map<File, ProcessedFile>;
|
||||||
|
|
||||||
// Current navigation state
|
// Current navigation state
|
||||||
|
currentMode: ModeType;
|
||||||
|
// Legacy fields for backward compatibility
|
||||||
currentView: ViewType;
|
currentView: ViewType;
|
||||||
currentTool: ToolType;
|
currentTool: ToolType;
|
||||||
|
|
||||||
@ -73,6 +77,8 @@ export interface FileContextActions {
|
|||||||
clearAllFiles: () => void;
|
clearAllFiles: () => void;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
|
setCurrentMode: (mode: ModeType) => void;
|
||||||
|
// Legacy navigation functions for backward compatibility
|
||||||
setCurrentView: (view: ViewType) => void;
|
setCurrentView: (view: ViewType) => void;
|
||||||
setCurrentTool: (tool: ToolType) => void;
|
setCurrentTool: (tool: ToolType) => void;
|
||||||
|
|
||||||
@ -134,6 +140,8 @@ export interface WithFileContext {
|
|||||||
|
|
||||||
// URL parameter types for deep linking
|
// URL parameter types for deep linking
|
||||||
export interface FileContextUrlParams {
|
export interface FileContextUrlParams {
|
||||||
|
mode?: ModeType;
|
||||||
|
// Legacy parameters for backward compatibility
|
||||||
view?: ViewType;
|
view?: ViewType;
|
||||||
tool?: ToolType;
|
tool?: ToolType;
|
||||||
fileIds?: string[];
|
fileIds?: string[];
|
||||||
|
Loading…
Reference in New Issue
Block a user