Merge pull request #117 from nevolodia/flip-video

Flip video tool added.
This commit is contained in:
Ibrahima G. Coulibaly
2025-05-22 17:19:27 +01:00
committed by GitHub
8 changed files with 198 additions and 13 deletions

16
.idea/workspace.xml generated
View File

@@ -6,7 +6,8 @@
<component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="fix: misc">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/input/BaseFileInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/BaseFileInput.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/pdf/protect-pdf/service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/protect-pdf/service.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -23,7 +24,7 @@
<option name="PUSH_AUTO_UPDATE" value="true" />
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="main" />
<entry key="$PROJECT_DIR$" value="fork/rohit267/feat/pdf-merge" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -134,6 +135,13 @@
"number": 102
},
"lastSeen": 1747171977348
},
{
"id": {
"id": "PR_kwDOMJIfts6XPua_",
"number": 117
},
"lastSeen": 1747929835864
}
]
}]]></component>
@@ -189,7 +197,7 @@
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
"Vitest.replaceText function.executor": "Run",
"Vitest.timeBetweenDates.executor": "Run",
"git-widget-placeholder": "#102 on fork/rohit267/feat/pdf-merge",
"git-widget-placeholder": "#117 on fork/nevolodia/flip-video",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
@@ -423,6 +431,8 @@
<workItem from="1745775228478" duration="1221000" />
<workItem from="1745835676024" duration="68000" />
<workItem from="1747171958176" duration="1105000" />
<workItem from="1747217211469" duration="4000" />
<workItem from="1747929815472" duration="843000" />
</task>
<task id="LOCAL-00147" summary="chore: update meta">
<option name="closed" value="true" />

View File

@@ -33,17 +33,14 @@ export default function BaseFileInput({
const { showSnackBar } = useContext(CustomSnackBarContext);
useEffect(() => {
try {
if (isArray(value)) {
const objectUrl = createObjectURL(value[0]);
if (value) {
try {
const objectUrl = createObjectURL(value);
setPreview(objectUrl);
return () => revokeObjectURL(objectUrl);
} else {
setPreview(null);
} catch (error) {
console.error('Error previewing file:', error);
}
} catch (error) {
console.error('Error previewing file:', error);
}
}, [value]);

View File

@@ -37,7 +37,6 @@ export async function protectPdf(
password: options.password
};
const protectedFileUrl: string = await protectWithGhostScript(dataObject);
console.log('protected', protectedFileUrl);
return await loadPDFData(
protectedFileUrl,
pdfFile.name.replace('.pdf', '-protected.pdf')

View File

@@ -0,0 +1,113 @@
import { Box } from '@mui/material';
import { useCallback, useState } from 'react';
import * as Yup from 'yup';
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 { flipVideo } from './service';
import { FlipOrientation, InitialValuesType } from './types';
import SimpleRadio from '@components/options/SimpleRadio';
export const initialValues: InitialValuesType = {
orientation: 'horizontal'
};
export const validationSchema = Yup.object({
orientation: Yup.string()
.oneOf(
['horizontal', 'vertical'],
'Orientation must be horizontal or vertical'
)
.required('Orientation is required')
});
const orientationOptions: { value: FlipOrientation; label: string }[] = [
{ value: 'horizontal', label: 'Horizontal (Mirror)' },
{ value: 'vertical', label: 'Vertical (Upside Down)' }
];
export default function FlipVideo({ title }: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const compute = async (
optionsValues: InitialValuesType,
input: File | null
) => {
if (!input) return;
setLoading(true);
try {
const flippedFile = await flipVideo(input, optionsValues.orientation);
setResult(flippedFile);
} catch (error) {
console.error('Error flipping video:', error);
} finally {
setLoading(false);
}
};
const debouncedCompute = useCallback(debounce(compute, 1000), []);
const getGroups: GetGroupsType<InitialValuesType> = ({
values,
updateField
}) => [
{
title: 'Orientation',
component: (
<Box>
{orientationOptions.map((orientationOption) => (
<SimpleRadio
key={orientationOption.value}
title={orientationOption.label}
checked={values.orientation === orientationOption.value}
onClick={() => {
updateField('orientation', orientationOption.value);
}}
/>
))}
</Box>
)
}
];
return (
<ToolContent
title={title}
input={input}
inputComponent={
<ToolVideoInput
value={input}
onChange={setInput}
title={'Input Video'}
/>
}
resultComponent={
loading ? (
<ToolFileResult
title={'Flipping Video'}
value={null}
loading={true}
extension={''}
/>
) : (
<ToolFileResult
title={'Flipped Video'}
value={result}
extension={'mp4'}
/>
)
}
initialValues={initialValues}
getGroups={getGroups}
compute={debouncedCompute}
setInput={setInput}
validationSchema={validationSchema}
/>
);
}

View File

@@ -0,0 +1,15 @@
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'))
});

View File

@@ -0,0 +1,43 @@
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile } from '@ffmpeg/util';
import { FlipOrientation } from './types';
const ffmpeg = new FFmpeg();
export async function flipVideo(
input: File,
orientation: FlipOrientation
): Promise<File> {
if (!ffmpeg.loaded) {
await ffmpeg.load({
wasmURL:
'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm'
});
}
const inputName = 'input.mp4';
const outputName = 'output.mp4';
await ffmpeg.writeFile(inputName, await fetchFile(input));
const flipMap: Record<FlipOrientation, string> = {
horizontal: 'hflip',
vertical: 'vflip'
};
const flipFilter = flipMap[orientation];
const args = ['-i', inputName];
if (flipFilter) {
args.push('-vf', flipFilter);
}
args.push('-c:v', 'libx264', '-preset', 'ultrafast', outputName);
await ffmpeg.exec(args);
const flippedData = await ffmpeg.readFile(outputName);
return new File(
[new Blob([flippedData], { type: 'video/mp4' })],
`${input.name.replace(/\.[^/.]+$/, '')}_flipped.mp4`,
{ type: 'video/mp4' }
);
}

View File

@@ -0,0 +1,5 @@
export type FlipOrientation = 'horizontal' | 'vertical';
export type InitialValuesType = {
orientation: FlipOrientation;
};

View File

@@ -1,14 +1,17 @@
import { tool as videoFlip } from './flip/meta';
import { rotate } from '../string/rotate/service';
import { gifTools } from './gif';
import { tool as trimVideo } from './trim/meta';
import { tool as rotateVideo } from './rotate/meta';
import { tool as compressVideo } from './compress/meta';
import { tool as loopVideo } from './loop/meta';
import { tool as flipVideo } from './flip/meta';
export const videoTools = [
...gifTools,
trimVideo,
rotateVideo,
compressVideo,
loopVideo
loopVideo,
flipVideo
];