mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge branch 'main' of https://github.com/iib0011/omni-tools into fork/AshAnand34/merge-video-tool
This commit is contained in:
@@ -9,6 +9,7 @@ import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { fetchFile } from '@ffmpeg/util';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
newSpeed: 2
|
||||
@@ -18,6 +19,7 @@ export default function ChangeSpeed({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const { t } = useTranslation('video');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -128,13 +130,13 @@ export default function ChangeSpeed({
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'New Video Speed',
|
||||
title: t('changeSpeed.newVideoSpeed'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
value={values.newSpeed.toString()}
|
||||
onOwnChange={(val) => updateField('newSpeed', Number(val))}
|
||||
description="Default multiplier: 2 means 2x faster"
|
||||
description={t('changeSpeed.defaultMultiplier')}
|
||||
type="number"
|
||||
/>
|
||||
</Box>
|
||||
@@ -149,21 +151,32 @@ export default function ChangeSpeed({
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('changeSpeed.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult title="Setting Speed" value={null} loading={true} />
|
||||
<ToolFileResult
|
||||
title={t('changeSpeed.settingSpeed')}
|
||||
value={null}
|
||||
loading={true}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult title="Edited Video" value={result} extension="mp4" />
|
||||
<ToolFileResult
|
||||
title={t('changeSpeed.resultTitle')}
|
||||
value={result}
|
||||
extension="mp4"
|
||||
/>
|
||||
)
|
||||
}
|
||||
initialValues={initialValues}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('changeSpeed.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Change speed',
|
||||
path: 'change-speed',
|
||||
icon: 'material-symbols-light:speed-outline',
|
||||
description:
|
||||
'This online utility lets you change the speed of a video. You can speed it up or slow it down.',
|
||||
shortDescription: 'Quickly change video speed',
|
||||
keywords: ['change', 'speed'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:speed',
|
||||
|
||||
keywords: ['video', 'speed', 'playback', 'fast', 'slow'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:changeSpeed.title',
|
||||
description: 'video:changeSpeed.description',
|
||||
shortDescription: 'video:changeSpeed.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { compressVideo, VideoResolution } from './service';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import Slider from 'rc-slider';
|
||||
import 'rc-slider/assets/index.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const initialValues = {
|
||||
width: 480 as VideoResolution,
|
||||
@@ -68,6 +69,7 @@ const presetOptions = [
|
||||
];
|
||||
|
||||
export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('video');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -100,7 +102,7 @@ export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Resolution',
|
||||
title: t('compress.resolution'),
|
||||
component: (
|
||||
<Box>
|
||||
{resolutionOptions.map((option) => (
|
||||
@@ -117,7 +119,7 @@ export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Quality (CRF)',
|
||||
title: t('compress.quality'),
|
||||
component: (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Slider
|
||||
@@ -129,9 +131,9 @@ export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
updateField('crf', typeof value === 'number' ? value : value[0]);
|
||||
}}
|
||||
marks={{
|
||||
0: 'Lossless',
|
||||
23: 'Default',
|
||||
51: 'Worst'
|
||||
0: t('compress.lossless'),
|
||||
23: t('compress.default'),
|
||||
51: t('compress.worst')
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -160,16 +162,16 @@ export default function CompressVideo({ title }: ToolComponentProps) {
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('compress.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Compressed Video'}
|
||||
title={t('compress.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
loading={loading}
|
||||
loadingText={'Compressing video...'}
|
||||
loadingText={t('compress.loadingText')}
|
||||
/>
|
||||
}
|
||||
initialValues={initialValues}
|
||||
|
||||
@@ -2,12 +2,9 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Compress Video',
|
||||
path: 'compress',
|
||||
icon: 'icon-park-outline:compression',
|
||||
description:
|
||||
'Compress videos by scaling them to different resolutions like 240p, 480p, 720p, etc. This tool helps reduce file size while maintaining acceptable quality. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Compress videos by scaling to different resolutions',
|
||||
|
||||
keywords: [
|
||||
'compress',
|
||||
'video',
|
||||
@@ -16,5 +13,10 @@ export const tool = defineTool('video', {
|
||||
'resolution',
|
||||
'reduce size'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:compress.title',
|
||||
description: 'video:compress.description',
|
||||
shortDescription: 'video:compress.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Box, TextField, Typography, Alert } from '@mui/material';
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import { Box, Typography, TextField, Alert } from '@mui/material';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
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';
|
||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import { debounce } from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
x: 0,
|
||||
@@ -17,6 +18,7 @@ const initialValues: InitialValuesType = {
|
||||
};
|
||||
|
||||
export default function CropVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('video');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -30,19 +32,23 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
if (!videoDimensions) return '';
|
||||
|
||||
if (values.x < 0 || values.y < 0) {
|
||||
return 'X and Y coordinates must be non-negative';
|
||||
return t('cropVideo.errorNonNegativeCoordinates');
|
||||
}
|
||||
|
||||
if (values.width <= 0 || values.height <= 0) {
|
||||
return 'Width and height must be positive';
|
||||
return t('cropVideo.errorPositiveDimensions');
|
||||
}
|
||||
|
||||
if (values.x + values.width > videoDimensions.width) {
|
||||
return `Crop area extends beyond video width (${videoDimensions.width}px)`;
|
||||
return t('cropVideo.errorBeyondWidth', {
|
||||
width: videoDimensions.width
|
||||
});
|
||||
}
|
||||
|
||||
if (values.y + values.height > videoDimensions.height) {
|
||||
return `Crop area extends beyond video height (${videoDimensions.height}px)`;
|
||||
return t('cropVideo.errorBeyondHeight', {
|
||||
height: videoDimensions.height
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -68,9 +74,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
setResult(croppedFile);
|
||||
} catch (error) {
|
||||
console.error('Error cropping video:', error);
|
||||
setProcessingError(
|
||||
'Error cropping video. Please check parameters and video file.'
|
||||
);
|
||||
setProcessingError(t('cropVideo.errorCroppingVideo'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -86,24 +90,26 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Video Information',
|
||||
title: t('cropVideo.videoInformation'),
|
||||
component: (
|
||||
<Box>
|
||||
{videoDimensions ? (
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
Video dimensions: {videoDimensions.width} ×{' '}
|
||||
{videoDimensions.height} pixels
|
||||
{t('cropVideo.videoDimensions', {
|
||||
width: videoDimensions.width,
|
||||
height: videoDimensions.height
|
||||
})}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ mb: 2 }}>
|
||||
Load a video to see dimensions
|
||||
{t('cropVideo.loadVideoForDimensions')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Crop Coordinates',
|
||||
title: t('cropVideo.cropCoordinates'),
|
||||
component: (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{processingError && (
|
||||
@@ -113,7 +119,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<TextField
|
||||
label="X (left)"
|
||||
label={t('cropVideo.xCoordinate')}
|
||||
type="number"
|
||||
value={values.x}
|
||||
onChange={(e) => updateField('x', parseInt(e.target.value) || 0)}
|
||||
@@ -121,7 +127,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
inputProps={{ min: 0 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Y (top)"
|
||||
label={t('cropVideo.yCoordinate')}
|
||||
type="number"
|
||||
value={values.y}
|
||||
onChange={(e) => updateField('y', parseInt(e.target.value) || 0)}
|
||||
@@ -131,7 +137,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<TextField
|
||||
label="Width"
|
||||
label={t('cropVideo.width')}
|
||||
type="number"
|
||||
value={values.width}
|
||||
onChange={(e) =>
|
||||
@@ -141,7 +147,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
inputProps={{ min: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Height"
|
||||
label={t('cropVideo.height')}
|
||||
type="number"
|
||||
value={values.height}
|
||||
onChange={(e) =>
|
||||
@@ -183,7 +189,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error getting video dimensions:', error);
|
||||
setProcessingError('Failed to load video dimensions');
|
||||
setProcessingError(t('cropVideo.errorLoadingDimensions'));
|
||||
});
|
||||
} else {
|
||||
setVideoDimensions(null);
|
||||
@@ -191,20 +197,20 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
||||
}
|
||||
setInput(video);
|
||||
}}
|
||||
title={'Input Video'}
|
||||
title={t('cropVideo.inputTitle')}
|
||||
/>
|
||||
)}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
title={'Cropping Video'}
|
||||
title={t('cropVideo.croppingVideo')}
|
||||
value={null}
|
||||
loading={true}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title={'Cropped Video'}
|
||||
title={t('cropVideo.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
|
||||
@@ -2,13 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Crop video',
|
||||
path: 'crop-video',
|
||||
icon: 'mdi:crop',
|
||||
description: 'Crop a video by specifying coordinates and dimensions',
|
||||
shortDescription: 'Crop video to specific area',
|
||||
keywords: ['crop', 'video', 'trim', 'cut', 'resize'],
|
||||
longDescription:
|
||||
'Remove unwanted parts from the edges of your video by cropping it to a specific rectangular area. Define the starting coordinates (X, Y) and the width and height of the area you want to keep.',
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:crop',
|
||||
|
||||
keywords: ['video', 'crop', 'trim', 'edit', 'resize'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:cropVideo.title',
|
||||
description: 'video:cropVideo.description',
|
||||
shortDescription: 'video:cropVideo.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { flipVideo } from './service';
|
||||
import { FlipOrientation, InitialValuesType } from './types';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const initialValues: InitialValuesType = {
|
||||
orientation: 'horizontal'
|
||||
@@ -30,6 +31,7 @@ const orientationOptions: { value: FlipOrientation; label: string }[] = [
|
||||
];
|
||||
|
||||
export default function FlipVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('video');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -58,13 +60,13 @@ export default function FlipVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Orientation',
|
||||
title: t('flip.orientation'),
|
||||
component: (
|
||||
<Box>
|
||||
{orientationOptions.map((orientationOption) => (
|
||||
<SimpleRadio
|
||||
key={orientationOption.value}
|
||||
title={orientationOption.label}
|
||||
title={t(`flip.${orientationOption.value}Label`)}
|
||||
checked={values.orientation === orientationOption.value}
|
||||
onClick={() => {
|
||||
updateField('orientation', orientationOption.value);
|
||||
@@ -84,20 +86,20 @@ export default function FlipVideo({ title }: ToolComponentProps) {
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('flip.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
title={'Flipping Video'}
|
||||
title={t('flip.flippingVideo')}
|
||||
value={null}
|
||||
loading={true}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title={'Flipped Video'}
|
||||
title={t('flip.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
|
||||
@@ -2,14 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Flip Video',
|
||||
path: 'flip',
|
||||
icon: 'mdi:flip-horizontal',
|
||||
description:
|
||||
'This online utility allows you to flip videos horizontally or vertically. You can preview the flipped video before processing. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Flip videos horizontally or vertically',
|
||||
keywords: ['flip', 'video', 'mirror', 'edit', 'horizontal', 'vertical'],
|
||||
longDescription:
|
||||
'Easily flip your videos horizontally (mirror) or vertically (upside down) with this simple online tool.',
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:flip',
|
||||
|
||||
keywords: ['video', 'flip', 'mirror', 'horizontal', 'vertical'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:flip.title',
|
||||
description: 'video:flip.description',
|
||||
shortDescription: 'video:flip.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,12 +3,14 @@ import { lazy } from 'react';
|
||||
// import image from '@assets/text.png';
|
||||
|
||||
export const tool = defineTool('gif', {
|
||||
name: 'Change speed',
|
||||
path: 'change-speed',
|
||||
icon: 'material-symbols-light:speed-outline',
|
||||
description:
|
||||
'This online utility lets you change the speed of a GIF animation. You can speed it up or slow it down. You can set the same constant delay between all frames or change the delays of individual frames. You can also play both the input and output GIFs at the same time and compare their speeds',
|
||||
shortDescription: 'Quickly change GIF speed',
|
||||
keywords: ['change', 'speed'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:speed',
|
||||
|
||||
keywords: ['gif', 'speed', 'animation', 'fast', 'slow'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:gif.changeSpeed.title',
|
||||
description: 'video:gif.changeSpeed.description',
|
||||
shortDescription: 'video:gif.changeSpeed.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import ToolFileResult from '@components/result/ToolFileResult';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { updateNumberField } from '@utils/string';
|
||||
import * as Yup from 'yup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
loops: 2
|
||||
@@ -21,6 +22,7 @@ const validationSchema = Yup.object({
|
||||
});
|
||||
|
||||
export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
const { t } = useTranslation('video');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -43,7 +45,7 @@ export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Loops',
|
||||
title: t('loop.loops'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
@@ -51,7 +53,7 @@ export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
updateNumberField(value, 'loops', updateField)
|
||||
}
|
||||
value={values.loops}
|
||||
label={'Number of Loops'}
|
||||
label={t('loop.numberOfLoops')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -66,14 +68,14 @@ export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
value={null}
|
||||
title={'Looping Video'}
|
||||
title={t('loop.loopingVideo')}
|
||||
loading={true}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
value={result}
|
||||
title={'Looped Video'}
|
||||
title={t('loop.resultTitle')}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
)
|
||||
@@ -83,7 +85,10 @@ export default function Loop({ title, longDescription }: ToolComponentProps) {
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
toolInfo={{
|
||||
title: t('loop.toolInfo.title', { title }),
|
||||
description: longDescription
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Loop Video',
|
||||
path: 'loop',
|
||||
icon: 'ic:baseline-loop',
|
||||
description:
|
||||
'This online utility lets you loop videos by specifying the number of repetitions. You can preview the looped video before processing. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Loop videos multiple times',
|
||||
keywords: ['loop', 'video', 'repeat', 'duplicate', 'sequence', 'playback'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:loop',
|
||||
|
||||
keywords: ['video', 'loop', 'repeat', 'continuous'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:loop.title',
|
||||
description: 'video:loop.description',
|
||||
shortDescription: 'video:loop.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { rotateVideo } from './service';
|
||||
import { RotationAngle } from '../../pdf/rotate-pdf/types';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const initialValues = {
|
||||
rotation: 90
|
||||
@@ -27,6 +28,7 @@ const angleOptions: { value: RotationAngle; label: string }[] = [
|
||||
{ value: 270, label: '270° (90° Counter-clockwise)' }
|
||||
];
|
||||
export default function RotateVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('video');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -55,13 +57,13 @@ export default function RotateVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Rotation',
|
||||
title: t('rotate.rotation'),
|
||||
component: (
|
||||
<Box>
|
||||
{angleOptions.map((angleOption) => (
|
||||
<SimpleRadio
|
||||
key={angleOption.value}
|
||||
title={angleOption.label}
|
||||
title={t(`rotate.${angleOption.value}Degrees`)}
|
||||
checked={values.rotation === angleOption.value}
|
||||
onClick={() => {
|
||||
updateField('rotation', angleOption.value);
|
||||
@@ -81,20 +83,20 @@ export default function RotateVideo({ title }: ToolComponentProps) {
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('rotate.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
title={'Rotating Video'}
|
||||
title={t('rotate.rotatingVideo')}
|
||||
value={null}
|
||||
loading={true}
|
||||
extension={''}
|
||||
/>
|
||||
) : (
|
||||
<ToolFileResult
|
||||
title={'Rotated Video'}
|
||||
title={t('rotate.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
|
||||
@@ -2,12 +2,14 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Rotate Video',
|
||||
path: 'rotate',
|
||||
icon: 'mdi:rotate-right',
|
||||
description:
|
||||
'This online utility lets you rotate videos by 90, 180, or 270 degrees. You can preview the rotated video before processing. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Rotate videos by 90, 180, or 270 degrees',
|
||||
keywords: ['rotate', 'video', 'flip', 'edit', 'adjust'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:rotate-right',
|
||||
|
||||
keywords: ['video', 'rotate', 'orientation', 'degrees'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:rotate.title',
|
||||
description: 'video:rotate.description',
|
||||
shortDescription: 'video:rotate.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||
import { fetchFile } from '@ffmpeg/util';
|
||||
import { debounce } from 'lodash';
|
||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ffmpeg = new FFmpeg();
|
||||
|
||||
@@ -28,6 +29,7 @@ const validationSchema = Yup.object({
|
||||
});
|
||||
|
||||
export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('video');
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
|
||||
@@ -85,7 +87,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Timestamps',
|
||||
title: t('trim.timestamps'),
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
@@ -93,7 +95,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
updateNumberField(value, 'trimStart', updateField)
|
||||
}
|
||||
value={values.trimStart}
|
||||
label={'Start Time'}
|
||||
label={t('trim.startTime')}
|
||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
@@ -101,7 +103,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
updateNumberField(value, 'trimEnd', updateField)
|
||||
}
|
||||
value={values.trimEnd}
|
||||
label={'End Time'}
|
||||
label={t('trim.endTime')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@@ -116,7 +118,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
title={t('trim.inputTitle')}
|
||||
showTrimControls={true}
|
||||
onTrimChange={(trimStart, trimEnd) => {
|
||||
setFieldValue('trimStart', trimStart);
|
||||
@@ -129,7 +131,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
}}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Trimmed Video'}
|
||||
title={t('trim.resultTitle')}
|
||||
value={result}
|
||||
extension={'mp4'}
|
||||
/>
|
||||
|
||||
@@ -2,12 +2,13 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Trim Video',
|
||||
path: 'trim',
|
||||
icon: 'mdi:scissors',
|
||||
description:
|
||||
'This online utility lets you trim videos by setting start and end points. You can preview the trimmed section before processing. Supports common video formats like MP4, WebM, and OGG.',
|
||||
shortDescription: 'Trim videos by setting start and end points',
|
||||
keywords: ['trim', 'cut', 'video', 'clip', 'edit'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:content-cut',
|
||||
keywords: ['video', 'trim', 'cut', 'edit', 'time'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:trim.title',
|
||||
description: 'video:trim.description',
|
||||
shortDescription: 'video:trim.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,11 +2,13 @@ import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
name: 'Video to Gif',
|
||||
path: 'video-to-gif',
|
||||
icon: 'fluent:gif-16-regular',
|
||||
description: 'This online utility lets you convert a short video to gif.',
|
||||
shortDescription: 'Quickly convert a short video to gif',
|
||||
keywords: ['video', 'to', 'gif', 'convert'],
|
||||
component: lazy(() => import('./index'))
|
||||
icon: 'material-symbols:gif',
|
||||
keywords: ['video', 'gif', 'convert', 'animated', 'image'],
|
||||
component: lazy(() => import('./index')),
|
||||
i18n: {
|
||||
name: 'video:videoToGif.title',
|
||||
description: 'video:videoToGif.description',
|
||||
shortDescription: 'video:videoToGif.shortDescription'
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user