mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
File Pinning
This commit is contained in:
parent
63c4d98fda
commit
c22ec2037d
@ -4,9 +4,12 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||||
import HistoryIcon from '@mui/icons-material/History';
|
import HistoryIcon from '@mui/icons-material/History';
|
||||||
|
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||||
|
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||||
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
||||||
import styles from './PageEditor.module.css';
|
import styles from './PageEditor.module.css';
|
||||||
import FileOperationHistory from '../history/FileOperationHistory';
|
import FileOperationHistory from '../history/FileOperationHistory';
|
||||||
|
import { useFileContext } from '../../contexts/FileContext';
|
||||||
|
|
||||||
interface FileItem {
|
interface FileItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -66,6 +69,10 @@ const FileThumbnail = ({
|
|||||||
}: FileThumbnailProps) => {
|
}: FileThumbnailProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [showHistory, setShowHistory] = useState(false);
|
const [showHistory, setShowHistory] = useState(false);
|
||||||
|
const { pinnedFiles, pinFile, unpinFile, isFilePinned, activeFiles } = useFileContext();
|
||||||
|
|
||||||
|
// Find the actual File object that corresponds to this FileItem
|
||||||
|
const actualFile = activeFiles.find(f => f.name === file.name && f.size === file.size);
|
||||||
|
|
||||||
const formatFileSize = (bytes: number) => {
|
const formatFileSize = (bytes: number) => {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
@ -301,6 +308,32 @@ const FileThumbnail = ({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{actualFile && (
|
||||||
|
<Tooltip label={isFilePinned(actualFile) ? "Unpin File" : "Pin File"}>
|
||||||
|
<ActionIcon
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
c={isFilePinned(actualFile) ? "yellow" : "white"}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isFilePinned(actualFile)) {
|
||||||
|
unpinFile(actualFile);
|
||||||
|
onSetStatus(`Unpinned ${file.name}`);
|
||||||
|
} else {
|
||||||
|
pinFile(actualFile);
|
||||||
|
onSetStatus(`Pinned ${file.name}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFilePinned(actualFile) ? (
|
||||||
|
<PushPinIcon style={{ fontSize: 20 }} />
|
||||||
|
) : (
|
||||||
|
<PushPinOutlinedIcon style={{ fontSize: 20 }} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip label="Close File">
|
<Tooltip label="Close File">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
size="md"
|
size="md"
|
||||||
|
@ -5,10 +5,13 @@ import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
|
|||||||
import StorageIcon from "@mui/icons-material/Storage";
|
import StorageIcon from "@mui/icons-material/Storage";
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import PushPinIcon from "@mui/icons-material/PushPin";
|
||||||
|
import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
|
||||||
|
|
||||||
import { FileWithUrl } from "../../types/file";
|
import { FileWithUrl } from "../../types/file";
|
||||||
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
import { getFileSize, getFileDate } from "../../utils/fileUtils";
|
||||||
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
import { useIndexedDBThumbnail } from "../../hooks/useIndexedDBThumbnail";
|
||||||
|
import { useFileContext } from "../../contexts/FileContext";
|
||||||
|
|
||||||
interface FileCardProps {
|
interface FileCardProps {
|
||||||
file: FileWithUrl;
|
file: FileWithUrl;
|
||||||
@ -25,6 +28,9 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file);
|
const { thumbnail: thumb, isGenerating } = useIndexedDBThumbnail(file);
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
const { pinFile, unpinFile, isFilePinned } = useFileContext();
|
||||||
|
|
||||||
|
const isPinned = isFilePinned(file as File);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -64,8 +70,25 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
|
|||||||
position: 'relative'
|
position: 'relative'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Pin indicator - always visible when pinned */}
|
||||||
|
{isPinned && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 4,
|
||||||
|
left: 4,
|
||||||
|
zIndex: 10,
|
||||||
|
backgroundColor: 'rgba(255, 165, 0, 0.9)',
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PushPinIcon style={{ fontSize: 16, color: 'white' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Hover action buttons */}
|
{/* Hover action buttons */}
|
||||||
{isHovered && (onView || onEdit) && (
|
{isHovered && (onView || onEdit || true) && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -80,6 +103,25 @@ const FileCard = ({ file, onRemove, onDoubleClick, onView, onEdit, isSelected, o
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
{/* Pin/Unpin button */}
|
||||||
|
<Tooltip label={isPinned ? "Unpin file (will be consumed by operations)" : "Pin file (won't be consumed by operations)"}>
|
||||||
|
<ActionIcon
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
color={isPinned ? "orange" : "gray"}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isPinned) {
|
||||||
|
unpinFile(file as File);
|
||||||
|
} else {
|
||||||
|
pinFile(file as File);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPinned ? <PushPinIcon style={{ fontSize: 16 }} /> : <PushPinOutlinedIcon style={{ fontSize: 16 }} />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{onView && (
|
{onView && (
|
||||||
<Tooltip label="View in Viewer">
|
<Tooltip label="View in Viewer">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
|
import { Button, Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CompressParameters } from "../../../hooks/tools/compress/useCompressOperation";
|
||||||
interface CompressParameters {
|
|
||||||
compressionMethod: 'quality' | 'filesize';
|
|
||||||
compressionLevel: number;
|
|
||||||
fileSizeValue: string;
|
|
||||||
fileSizeUnit: 'KB' | 'MB';
|
|
||||||
grayscale: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CompressSettingsProps {
|
interface CompressSettingsProps {
|
||||||
parameters: CompressParameters;
|
parameters: CompressParameters;
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text } from '@mantine/core';
|
import { Text, Box, Flex, ActionIcon, Tooltip } from '@mantine/core';
|
||||||
|
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||||
|
import PushPinOutlinedIcon from '@mui/icons-material/PushPinOutlined';
|
||||||
|
import { useFileContext } from '../../../contexts/FileContext';
|
||||||
|
|
||||||
export interface FileStatusIndicatorProps {
|
export interface FileStatusIndicatorProps {
|
||||||
selectedFiles?: File[];
|
selectedFiles?: File[];
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
showFileName?: boolean;
|
showFileName?: boolean;
|
||||||
|
showPinControls?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileStatusIndicator = ({
|
const FileStatusIndicator = ({
|
||||||
selectedFiles = [],
|
selectedFiles = [],
|
||||||
isCompleted = false,
|
isCompleted = false,
|
||||||
placeholder = "Select a PDF file in the main view to get started",
|
placeholder = "Select a PDF file in the main view to get started",
|
||||||
showFileName = true
|
showFileName = true,
|
||||||
|
showPinControls = true
|
||||||
}: FileStatusIndicatorProps) => {
|
}: FileStatusIndicatorProps) => {
|
||||||
|
const { pinFile, unpinFile, isFilePinned } = useFileContext();
|
||||||
if (selectedFiles.length === 0) {
|
if (selectedFiles.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Stack } from '@mantine/core';
|
|
||||||
import { createToolSteps, ToolStepProvider } from './ToolStep';
|
import { createToolSteps, ToolStepProvider } from './ToolStep';
|
||||||
import OperationButton from './OperationButton';
|
import OperationButton from './OperationButton';
|
||||||
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
import { ToolOperationHook } from '../../../hooks/tools/shared/useToolOperation';
|
||||||
@ -57,7 +56,7 @@ export interface ToolFlowConfig {
|
|||||||
*/
|
*/
|
||||||
export function createToolFlow(config: ToolFlowConfig) {
|
export function createToolFlow(config: ToolFlowConfig) {
|
||||||
const steps = createToolSteps();
|
const steps = createToolSteps();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolStepProvider>
|
<ToolStepProvider>
|
||||||
{/* Files Step */}
|
{/* Files Step */}
|
||||||
@ -69,7 +68,7 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Middle Steps */}
|
{/* Middle Steps */}
|
||||||
{config.steps.map((stepConfig, index) =>
|
{config.steps.map((stepConfig, index) =>
|
||||||
steps.create(stepConfig.title, {
|
steps.create(stepConfig.title, {
|
||||||
isVisible: stepConfig.isVisible,
|
isVisible: stepConfig.isVisible,
|
||||||
isCollapsed: stepConfig.isCollapsed,
|
isCollapsed: stepConfig.isCollapsed,
|
||||||
@ -99,4 +98,4 @@ export function createToolFlow(config: ToolFlowConfig) {
|
|||||||
})}
|
})}
|
||||||
</ToolStepProvider>
|
</ToolStepProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ const initialViewerConfig: ViewerConfig = {
|
|||||||
const initialState: FileContextState = {
|
const initialState: FileContextState = {
|
||||||
activeFiles: [],
|
activeFiles: [],
|
||||||
processedFiles: new Map(),
|
processedFiles: new Map(),
|
||||||
|
pinnedFiles: new Set(),
|
||||||
currentMode: 'pageEditor',
|
currentMode: 'pageEditor',
|
||||||
currentView: 'fileEditor', // Legacy field
|
currentView: 'fileEditor', // Legacy field
|
||||||
currentTool: null, // Legacy field
|
currentTool: null, // Legacy field
|
||||||
@ -77,6 +78,9 @@ type FileContextAction =
|
|||||||
| { type: 'SET_UNSAVED_CHANGES'; payload: boolean }
|
| { type: 'SET_UNSAVED_CHANGES'; payload: boolean }
|
||||||
| { type: 'SET_PENDING_NAVIGATION'; payload: (() => void) | null }
|
| { type: 'SET_PENDING_NAVIGATION'; payload: (() => void) | null }
|
||||||
| { type: 'SHOW_NAVIGATION_WARNING'; payload: boolean }
|
| { type: 'SHOW_NAVIGATION_WARNING'; payload: boolean }
|
||||||
|
| { type: 'PIN_FILE'; payload: File }
|
||||||
|
| { type: 'UNPIN_FILE'; payload: File }
|
||||||
|
| { type: 'CONSUME_FILES'; payload: { inputFiles: File[]; outputFiles: File[] } }
|
||||||
| { type: 'RESET_CONTEXT' }
|
| { type: 'RESET_CONTEXT' }
|
||||||
| { type: 'LOAD_STATE'; payload: Partial<FileContextState> };
|
| { type: 'LOAD_STATE'; payload: Partial<FileContextState> };
|
||||||
|
|
||||||
@ -317,6 +321,43 @@ function fileContextReducer(state: FileContextState, action: FileContextAction):
|
|||||||
showNavigationWarning: action.payload
|
showNavigationWarning: action.payload
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case 'PIN_FILE':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pinnedFiles: new Set([...state.pinnedFiles, action.payload])
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'UNPIN_FILE':
|
||||||
|
const newPinnedFiles = new Set(state.pinnedFiles);
|
||||||
|
newPinnedFiles.delete(action.payload);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pinnedFiles: newPinnedFiles
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'CONSUME_FILES': {
|
||||||
|
const { inputFiles, outputFiles } = action.payload;
|
||||||
|
const unpinnedInputFiles = inputFiles.filter(file => !state.pinnedFiles.has(file));
|
||||||
|
|
||||||
|
// Remove unpinned input files and add output files
|
||||||
|
const newActiveFiles = [
|
||||||
|
...state.activeFiles.filter(file => !unpinnedInputFiles.includes(file)),
|
||||||
|
...outputFiles
|
||||||
|
];
|
||||||
|
|
||||||
|
// Update processed files map - remove consumed files, keep pinned ones
|
||||||
|
const newProcessedFiles = new Map(state.processedFiles);
|
||||||
|
unpinnedInputFiles.forEach(file => {
|
||||||
|
newProcessedFiles.delete(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeFiles: newActiveFiles,
|
||||||
|
processedFiles: newProcessedFiles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case 'RESET_CONTEXT':
|
case 'RESET_CONTEXT':
|
||||||
return {
|
return {
|
||||||
...initialState
|
...initialState
|
||||||
@ -562,6 +603,46 @@ export function FileContextProvider({
|
|||||||
dispatch({ type: 'CLEAR_SELECTIONS' });
|
dispatch({ type: 'CLEAR_SELECTIONS' });
|
||||||
}, [cleanupAllFiles]);
|
}, [cleanupAllFiles]);
|
||||||
|
|
||||||
|
// File pinning functions
|
||||||
|
const pinFile = useCallback((file: File) => {
|
||||||
|
dispatch({ type: 'PIN_FILE', payload: file });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unpinFile = useCallback((file: File) => {
|
||||||
|
dispatch({ type: 'UNPIN_FILE', payload: file });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isFilePinned = useCallback((file: File): boolean => {
|
||||||
|
return state.pinnedFiles.has(file);
|
||||||
|
}, [state.pinnedFiles]);
|
||||||
|
|
||||||
|
// File consumption function
|
||||||
|
const consumeFiles = useCallback(async (inputFiles: File[], outputFiles: File[]): Promise<void> => {
|
||||||
|
dispatch({ type: 'CONSUME_FILES', payload: { inputFiles, outputFiles } });
|
||||||
|
|
||||||
|
// Store new output files if persistence is enabled
|
||||||
|
if (enablePersistence) {
|
||||||
|
for (const file of outputFiles) {
|
||||||
|
try {
|
||||||
|
const fileId = getFileId(file);
|
||||||
|
if (!fileId) {
|
||||||
|
try {
|
||||||
|
const thumbnail = await (thumbnailGenerationService as any).generateThumbnail(file);
|
||||||
|
const storedFile = await fileStorage.storeFile(file, thumbnail);
|
||||||
|
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
||||||
|
} catch (thumbnailError) {
|
||||||
|
console.warn('Failed to generate thumbnail, storing without:', thumbnailError);
|
||||||
|
const storedFile = await fileStorage.storeFile(file);
|
||||||
|
Object.defineProperty(file, 'id', { value: storedFile.id, writable: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to store output file:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [enablePersistence, state.pinnedFiles]);
|
||||||
|
|
||||||
// Navigation guard system functions
|
// Navigation guard system functions
|
||||||
const setHasUnsavedChanges = useCallback((hasChanges: boolean) => {
|
const setHasUnsavedChanges = useCallback((hasChanges: boolean) => {
|
||||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: hasChanges });
|
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: hasChanges });
|
||||||
@ -785,6 +866,10 @@ export function FileContextProvider({
|
|||||||
removeFiles,
|
removeFiles,
|
||||||
replaceFile,
|
replaceFile,
|
||||||
clearAllFiles,
|
clearAllFiles,
|
||||||
|
pinFile,
|
||||||
|
unpinFile,
|
||||||
|
isFilePinned,
|
||||||
|
consumeFiles,
|
||||||
setCurrentMode,
|
setCurrentMode,
|
||||||
setCurrentView,
|
setCurrentView,
|
||||||
setCurrentTool,
|
setCurrentTool,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { createContext, useContext, useState, useCallback, ReactNode } from 'react';
|
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
MaxFiles,
|
MaxFiles,
|
||||||
FileSelectionContextValue
|
FileSelectionContextValue
|
||||||
} from '../types/tool';
|
} from '../types/tool';
|
||||||
|
import { useFileContext } from './FileContext';
|
||||||
|
|
||||||
interface FileSelectionProviderProps {
|
interface FileSelectionProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -11,10 +12,23 @@ interface FileSelectionProviderProps {
|
|||||||
const FileSelectionContext = createContext<FileSelectionContextValue | undefined>(undefined);
|
const FileSelectionContext = createContext<FileSelectionContextValue | undefined>(undefined);
|
||||||
|
|
||||||
export function FileSelectionProvider({ children }: FileSelectionProviderProps) {
|
export function FileSelectionProvider({ children }: FileSelectionProviderProps) {
|
||||||
|
const { activeFiles } = useFileContext();
|
||||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||||
const [maxFiles, setMaxFiles] = useState<MaxFiles>(-1);
|
const [maxFiles, setMaxFiles] = useState<MaxFiles>(-1);
|
||||||
const [isToolMode, setIsToolMode] = useState<boolean>(false);
|
const [isToolMode, setIsToolMode] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Sync selected files with active files - remove any selected files that are no longer active
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFiles.length > 0) {
|
||||||
|
const activeFileSet = new Set(activeFiles);
|
||||||
|
const validSelectedFiles = selectedFiles.filter(file => activeFileSet.has(file));
|
||||||
|
|
||||||
|
if (validSelectedFiles.length !== selectedFiles.length) {
|
||||||
|
setSelectedFiles(validSelectedFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeFiles, selectedFiles]);
|
||||||
|
|
||||||
const clearSelection = useCallback(() => {
|
const clearSelection = useCallback(() => {
|
||||||
setSelectedFiles([]);
|
setSelectedFiles([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -11,7 +11,7 @@ export interface CompressParameters {
|
|||||||
fileSizeUnit: 'KB' | 'MB';
|
fileSizeUnit: 'KB' | 'MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildFormData = (parameters: CompressParameters, file: File): FormData => {
|
const buildFormData = (file: File, parameters: CompressParameters): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export const useToolOperation = <TParams = void>(
|
|||||||
config: ToolOperationConfig<TParams>
|
config: ToolOperationConfig<TParams>
|
||||||
): ToolOperationHook<TParams> => {
|
): ToolOperationHook<TParams> => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { recordOperation, markOperationApplied, markOperationFailed, addFiles } = useFileContext();
|
const { recordOperation, markOperationApplied, markOperationFailed, addFiles, consumeFiles } = useFileContext();
|
||||||
|
|
||||||
// Composed hooks
|
// Composed hooks
|
||||||
const { state, actions } = useToolState();
|
const { state, actions } = useToolState();
|
||||||
@ -198,8 +198,8 @@ export const useToolOperation = <TParams = void>(
|
|||||||
actions.setThumbnails(thumbnails);
|
actions.setThumbnails(thumbnails);
|
||||||
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
actions.setDownloadInfo(downloadInfo.url, downloadInfo.filename);
|
||||||
|
|
||||||
// Add to file context
|
// Consume input files and add output files (will replace unpinned inputs)
|
||||||
await addFiles(processedFiles);
|
await consumeFiles(validFiles, processedFiles);
|
||||||
|
|
||||||
markOperationApplied(fileId, operationId);
|
markOperationApplied(fileId, operationId);
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
compressOperation.resetResults();
|
compressOperation.resetResults();
|
||||||
onPreviewFile?.(null);
|
onPreviewFile?.(null);
|
||||||
}, [compressParams.parameters, selectedFiles]);
|
}, [compressParams.parameters]);
|
||||||
|
|
||||||
const handleCompress = async () => {
|
const handleCompress = async () => {
|
||||||
try {
|
try {
|
||||||
@ -61,7 +61,6 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
|
|
||||||
const hasFiles = selectedFiles.length > 0;
|
const hasFiles = selectedFiles.length > 0;
|
||||||
const hasResults = compressOperation.files.length > 0 || compressOperation.downloadUrl !== null;
|
const hasResults = compressOperation.files.length > 0 || compressOperation.downloadUrl !== null;
|
||||||
const filesCollapsed = hasFiles;
|
|
||||||
const settingsCollapsed = !hasFiles || hasResults;
|
const settingsCollapsed = !hasFiles || hasResults;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -69,7 +68,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
{createToolFlow({
|
{createToolFlow({
|
||||||
files: {
|
files: {
|
||||||
selectedFiles,
|
selectedFiles,
|
||||||
isCollapsed: filesCollapsed
|
isCollapsed: hasFiles
|
||||||
},
|
},
|
||||||
steps: [{
|
steps: [{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
@ -86,6 +85,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
}],
|
}],
|
||||||
executeButton: {
|
executeButton: {
|
||||||
text: t("compress.submit", "Compress"),
|
text: t("compress.submit", "Compress"),
|
||||||
|
isVisible: !hasResults,
|
||||||
loadingText: t("loading"),
|
loadingText: t("loading"),
|
||||||
onClick: handleCompress,
|
onClick: handleCompress,
|
||||||
disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled
|
disabled: !compressParams.validateParameters() || !hasFiles || !endpointEnabled
|
||||||
|
@ -55,6 +55,7 @@ export interface FileContextState {
|
|||||||
// Core file management
|
// Core file management
|
||||||
activeFiles: File[];
|
activeFiles: File[];
|
||||||
processedFiles: Map<File, ProcessedFile>;
|
processedFiles: Map<File, ProcessedFile>;
|
||||||
|
pinnedFiles: Set<File>; // Files that are pinned and won't be consumed
|
||||||
|
|
||||||
// Current navigation state
|
// Current navigation state
|
||||||
currentMode: ModeType;
|
currentMode: ModeType;
|
||||||
@ -95,6 +96,14 @@ export interface FileContextActions {
|
|||||||
removeFiles: (fileIds: string[], deleteFromStorage?: boolean) => void;
|
removeFiles: (fileIds: string[], deleteFromStorage?: boolean) => void;
|
||||||
replaceFile: (oldFileId: string, newFile: File) => Promise<void>;
|
replaceFile: (oldFileId: string, newFile: File) => Promise<void>;
|
||||||
clearAllFiles: () => void;
|
clearAllFiles: () => void;
|
||||||
|
|
||||||
|
// File pinning
|
||||||
|
pinFile: (file: File) => void;
|
||||||
|
unpinFile: (file: File) => void;
|
||||||
|
isFilePinned: (file: File) => boolean;
|
||||||
|
|
||||||
|
// File consumption (replace unpinned files with outputs)
|
||||||
|
consumeFiles: (inputFiles: File[], outputFiles: File[]) => Promise<void>;
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
setCurrentMode: (mode: ModeType) => void;
|
setCurrentMode: (mode: ModeType) => void;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user