diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 59e1902..8b52a7f 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -6,7 +6,8 @@
-
+
+
@@ -23,7 +24,7 @@
@@ -134,6 +135,13 @@
"number": 102
},
"lastSeen": 1747171977348
+ },
+ {
+ "id": {
+ "id": "PR_kwDOMJIfts6XPua_",
+ "number": 117
+ },
+ "lastSeen": 1747929835864
}
]
}]]>
@@ -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 @@
+
+
diff --git a/src/components/input/BaseFileInput.tsx b/src/components/input/BaseFileInput.tsx
index 0dd42a5..6346b6d 100644
--- a/src/components/input/BaseFileInput.tsx
+++ b/src/components/input/BaseFileInput.tsx
@@ -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]);
diff --git a/src/pages/tools/pdf/protect-pdf/service.ts b/src/pages/tools/pdf/protect-pdf/service.ts
index 6619a50..058a78c 100644
--- a/src/pages/tools/pdf/protect-pdf/service.ts
+++ b/src/pages/tools/pdf/protect-pdf/service.ts
@@ -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')
diff --git a/src/pages/tools/video/flip/index.tsx b/src/pages/tools/video/flip/index.tsx
new file mode 100644
index 0000000..c01fd41
--- /dev/null
+++ b/src/pages/tools/video/flip/index.tsx
@@ -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(null);
+ const [result, setResult] = useState(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 = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'Orientation',
+ component: (
+
+ {orientationOptions.map((orientationOption) => (
+ {
+ updateField('orientation', orientationOption.value);
+ }}
+ />
+ ))}
+
+ )
+ }
+ ];
+
+ return (
+
+ }
+ resultComponent={
+ loading ? (
+
+ ) : (
+
+ )
+ }
+ initialValues={initialValues}
+ getGroups={getGroups}
+ compute={debouncedCompute}
+ setInput={setInput}
+ validationSchema={validationSchema}
+ />
+ );
+}
diff --git a/src/pages/tools/video/flip/meta.ts b/src/pages/tools/video/flip/meta.ts
new file mode 100644
index 0000000..3462925
--- /dev/null
+++ b/src/pages/tools/video/flip/meta.ts
@@ -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'))
+});
diff --git a/src/pages/tools/video/flip/service.ts b/src/pages/tools/video/flip/service.ts
new file mode 100644
index 0000000..873d769
--- /dev/null
+++ b/src/pages/tools/video/flip/service.ts
@@ -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 {
+ 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 = {
+ 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' }
+ );
+}
diff --git a/src/pages/tools/video/flip/types.ts b/src/pages/tools/video/flip/types.ts
new file mode 100644
index 0000000..6f2258c
--- /dev/null
+++ b/src/pages/tools/video/flip/types.ts
@@ -0,0 +1,5 @@
+export type FlipOrientation = 'horizontal' | 'vertical';
+
+export type InitialValuesType = {
+ orientation: FlipOrientation;
+};
diff --git a/src/pages/tools/video/index.ts b/src/pages/tools/video/index.ts
index ca3e413..3e6659d 100644
--- a/src/pages/tools/video/index.ts
+++ b/src/pages/tools/video/index.ts
@@ -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
];