import { Box, TextField, Typography, Alert } from '@mui/material'; import { useCallback, useState, useEffect } from 'react'; import ToolFileResult from '@components/result/ToolFileResult'; import ToolContent from '@components/ToolContent'; import { ToolComponentProps } from '@tools/defineTool'; import { GetGroupsType } from '@components/options/ToolOptions'; import { debounce } from 'lodash'; import ToolVideoInput from '@components/input/ToolVideoInput'; import { cropVideo, getVideoDimensions } from './service'; import { InitialValuesType } from './types'; const initialValues: InitialValuesType = { x: 0, y: 0, width: 100, height: 100 }; export default function CropVideo({ title }: ToolComponentProps) { const [input, setInput] = useState(null); const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [videoDimensions, setVideoDimensions] = useState<{ width: number; height: number; } | null>(null); const [processingError, setProcessingError] = useState(''); const validateDimensions = (values: InitialValuesType): string => { if (!videoDimensions) return ''; if (values.x < 0 || values.y < 0) { return 'X and Y coordinates must be non-negative'; } if (values.width <= 0 || values.height <= 0) { return 'Width and height must be positive'; } if (values.x + values.width > videoDimensions.width) { return `Crop area extends beyond video width (${videoDimensions.width}px)`; } if (values.y + values.height > videoDimensions.height) { return `Crop area extends beyond video height (${videoDimensions.height}px)`; } return ''; }; const compute = async ( optionsValues: InitialValuesType, input: File | null ) => { if (!input) return; const error = validateDimensions(optionsValues); if (error) { setProcessingError(error); return; } setProcessingError(''); setLoading(true); try { const croppedFile = await cropVideo(input, optionsValues); setResult(croppedFile); } catch (error) { console.error('Error cropping video:', error); setProcessingError( 'Error cropping video. Please check parameters and video file.' ); } finally { setLoading(false); } }; // 2 seconds to avoid starting job half way through const debouncedCompute = useCallback(debounce(compute, 2000), [ videoDimensions ]); const getGroups: GetGroupsType = ({ values, updateField }) => [ { title: 'Video Information', component: ( {videoDimensions ? ( Video dimensions: {videoDimensions.width} ×{' '} {videoDimensions.height} pixels ) : ( Load a video to see dimensions )} ) }, { title: 'Crop Coordinates', component: ( {processingError && ( {processingError} )} updateField('x', parseInt(e.target.value) || 0)} size="small" inputProps={{ min: 0 }} /> updateField('y', parseInt(e.target.value) || 0)} size="small" inputProps={{ min: 0 }} /> updateField('width', parseInt(e.target.value) || 0) } size="small" inputProps={{ min: 1 }} /> updateField('height', parseInt(e.target.value) || 0) } size="small" inputProps={{ min: 1 }} /> ) } ]; return ( ( { if (video) { getVideoDimensions(video) .then((dimensions) => { const newOptions: InitialValuesType = { x: dimensions.width / 4, y: dimensions.height / 4, width: dimensions.width / 2, height: dimensions.height / 2 }; setFieldValue('x', newOptions.x); setFieldValue('y', newOptions.y); setFieldValue('width', newOptions.width); setFieldValue('height', newOptions.height); setVideoDimensions(dimensions); setProcessingError(''); }) .catch((error) => { console.error('Error getting video dimensions:', error); setProcessingError('Failed to load video dimensions'); }); } else { setVideoDimensions(null); setProcessingError(''); } setInput(video); }} title={'Input Video'} /> )} resultComponent={ loading ? ( ) : ( ) } initialValues={initialValues} getGroups={getGroups} compute={debouncedCompute} setInput={setInput} /> ); }