mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
chore: crop video
This commit is contained in:
143
.idea/workspace.xml
generated
143
.idea/workspace.xml
generated
@@ -4,14 +4,15 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: readme img and fix broken link">
|
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: crop video init">
|
||||||
<change afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/crop-video.service.test.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.github/workflows/ci.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.github/workflows/ci.yml" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/index.tsx" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/meta.ts" afterDir="false" />
|
|
||||||
<change afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/types.ts" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/components/ToolContent.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ToolContent.tsx" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/index.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/components/input/ToolVideoInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/ToolVideoInput.tsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/crop-video/crop-video.service.test.ts" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/crop-video/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/index.tsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/crop-video/types.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/crop-video/types.ts" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -163,57 +164,57 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent">{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"Docker.Dockerfile build.executor": "Run",
|
"Docker.Dockerfile build.executor": "Run",
|
||||||
"Docker.Dockerfile.executor": "Run",
|
"Docker.Dockerfile.executor": "Run",
|
||||||
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
||||||
"Playwright.JoinText Component.executor": "Run",
|
"Playwright.JoinText Component.executor": "Run",
|
||||||
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
||||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"Vitest.compute function (1).executor": "Run",
|
"Vitest.compute function (1).executor": "Run",
|
||||||
"Vitest.compute function.executor": "Run",
|
"Vitest.compute function.executor": "Run",
|
||||||
"Vitest.crop-video.executor": "Run",
|
"Vitest.crop-video.executor": "Run",
|
||||||
"Vitest.mergeText.executor": "Run",
|
"Vitest.mergeText.executor": "Run",
|
||||||
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
|
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
|
||||||
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
||||||
"Vitest.parsePageRanges.executor": "Run",
|
"Vitest.parsePageRanges.executor": "Run",
|
||||||
"Vitest.removeDuplicateLines function.executor": "Run",
|
"Vitest.removeDuplicateLines function.executor": "Run",
|
||||||
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
|
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
|
||||||
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
|
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
|
||||||
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||||
"Vitest.replaceText function.executor": "Run",
|
"Vitest.replaceText function.executor": "Run",
|
||||||
"Vitest.timeBetweenDates.executor": "Run",
|
"Vitest.timeBetweenDates.executor": "Run",
|
||||||
"git-widget-placeholder": "crop-video",
|
"git-widget-placeholder": "crop-video",
|
||||||
"ignore.virus.scanning.warn.message": "true",
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
"kotlin-language-version-configured": "true",
|
"kotlin-language-version-configured": "true",
|
||||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
|
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"npm.build.executor": "Run",
|
"npm.build.executor": "Run",
|
||||||
"npm.dev.executor": "Run",
|
"npm.dev.executor": "Run",
|
||||||
"npm.lint.executor": "Run",
|
"npm.lint.executor": "Run",
|
||||||
"npm.prebuild.executor": "Run",
|
"npm.prebuild.executor": "Run",
|
||||||
"npm.script:create:tool.executor": "Run",
|
"npm.script:create:tool.executor": "Run",
|
||||||
"npm.test.executor": "Run",
|
"npm.test.executor": "Run",
|
||||||
"npm.test:e2e.executor": "Run",
|
"npm.test:e2e.executor": "Run",
|
||||||
"npm.test:e2e:run.executor": "Run",
|
"npm.test:e2e:run.executor": "Run",
|
||||||
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
||||||
"project.structure.last.edited": "Problems",
|
"project.structure.last.edited": "Problems",
|
||||||
"project.structure.proportion": "0.0",
|
"project.structure.proportion": "0.0",
|
||||||
"project.structure.side.proportion": "0.2",
|
"project.structure.side.proportion": "0.2",
|
||||||
"settings.editor.selected.configurable": "refactai_advanced_settings",
|
"settings.editor.selected.configurable": "refactai_advanced_settings",
|
||||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}]]></component>
|
}</component>
|
||||||
<component name="ReactDesignerToolWindowState">
|
<component name="ReactDesignerToolWindowState">
|
||||||
<option name="myId2Visible">
|
<option name="myId2Visible">
|
||||||
<map>
|
<map>
|
||||||
@@ -239,7 +240,7 @@
|
|||||||
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\pages\categories" />
|
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\pages\categories" />
|
||||||
</key>
|
</key>
|
||||||
</component>
|
</component>
|
||||||
<component name="RunManager" selected="Vitest.crop-video">
|
<component name="RunManager" selected="npm.dev">
|
||||||
<configuration name="crop-video" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
|
<configuration name="crop-video" type="JavaScriptTestRunnerVitest" temporary="true" nameIsGenerated="true">
|
||||||
<node-interpreter value="project" />
|
<node-interpreter value="project" />
|
||||||
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
|
<vitest-package value="$PROJECT_DIR$/node_modules/vitest" />
|
||||||
@@ -318,8 +319,8 @@
|
|||||||
</list>
|
</list>
|
||||||
<recent_temporary>
|
<recent_temporary>
|
||||||
<list>
|
<list>
|
||||||
<item itemvalue="Vitest.crop-video" />
|
|
||||||
<item itemvalue="npm.dev" />
|
<item itemvalue="npm.dev" />
|
||||||
|
<item itemvalue="Vitest.crop-video" />
|
||||||
<item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" />
|
<item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" />
|
||||||
<item itemvalue="Vitest.parsePageRanges" />
|
<item itemvalue="Vitest.parsePageRanges" />
|
||||||
<item itemvalue="Vitest.timeBetweenDates" />
|
<item itemvalue="Vitest.timeBetweenDates" />
|
||||||
@@ -417,14 +418,8 @@
|
|||||||
<workItem from="1743699386059" duration="11195000" />
|
<workItem from="1743699386059" duration="11195000" />
|
||||||
<workItem from="1743782726563" duration="2444000" />
|
<workItem from="1743782726563" duration="2444000" />
|
||||||
<workItem from="1743811558991" duration="1279000" />
|
<workItem from="1743811558991" duration="1279000" />
|
||||||
</task>
|
<workItem from="1744042155791" duration="11000" />
|
||||||
<task id="LOCAL-00144" summary="feat: stringify json">
|
<workItem from="1744051596076" duration="361000" />
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1741417920442</created>
|
|
||||||
<option name="number" value="00144" />
|
|
||||||
<option name="presentableId" value="LOCAL-00144" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1741417920442</updated>
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00145" summary="feat: arithmetic sequence">
|
<task id="LOCAL-00145" summary="feat: arithmetic sequence">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -810,7 +805,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1743811980098</updated>
|
<updated>1743811980098</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="193" />
|
<task id="LOCAL-00193" summary="feat: crop video init">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1743986670751</created>
|
||||||
|
<option name="number" value="00193" />
|
||||||
|
<option name="presentableId" value="LOCAL-00193" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1743986670751</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="194" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -857,7 +860,6 @@
|
|||||||
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
||||||
<option name="CHECK_NEW_TODO" value="false" />
|
<option name="CHECK_NEW_TODO" value="false" />
|
||||||
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
||||||
<MESSAGE value="chore: result file name" />
|
|
||||||
<MESSAGE value="chore: text result extensions" />
|
<MESSAGE value="chore: text result extensions" />
|
||||||
<MESSAGE value="chore: show new tools in landing" />
|
<MESSAGE value="chore: show new tools in landing" />
|
||||||
<MESSAGE value="chore: zoom on hover" />
|
<MESSAGE value="chore: zoom on hover" />
|
||||||
@@ -882,7 +884,8 @@
|
|||||||
<MESSAGE value="feat: image to text" />
|
<MESSAGE value="feat: image to text" />
|
||||||
<MESSAGE value="chore: hideCopy if video or audio" />
|
<MESSAGE value="chore: hideCopy if video or audio" />
|
||||||
<MESSAGE value="chore: readme img and fix broken link" />
|
<MESSAGE value="chore: readme img and fix broken link" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="chore: readme img and fix broken link" />
|
<MESSAGE value="feat: crop video init" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="feat: crop video init" />
|
||||||
</component>
|
</component>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -37,6 +37,7 @@
|
|||||||
"rc-slider": "^11.1.8",
|
"rc-slider": "^11.1.8",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-easy-crop": "^5.4.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-image-crop": "^11.0.7",
|
"react-image-crop": "^11.0.7",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
@@ -7876,6 +7877,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-wheel": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/notistack": {
|
"node_modules/notistack": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz",
|
||||||
@@ -8884,6 +8891,20 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-easy-crop": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-Djtsi7bWO75vkKYkVxNRrJWY69pXLahIAkUN0mmt9cXNnaq2tpG59ctSY6P7ipJgBc7COJDRMRuwb2lYwtACNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"normalize-wheel": "^1.0.1",
|
||||||
|
"tslib": "^2.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.4.0",
|
||||||
|
"react-dom": ">=16.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-fast-compare": {
|
"node_modules/react-fast-compare": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"rc-slider": "^11.1.8",
|
"rc-slider": "^11.1.8",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-easy-crop": "^5.4.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-image-crop": "^11.0.7",
|
"react-image-crop": "^11.0.7",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import Slider from 'rc-slider';
|
import Slider from 'rc-slider';
|
||||||
import 'rc-slider/assets/index.css';
|
import 'rc-slider/assets/index.css';
|
||||||
import BaseFileInput from './BaseFileInput';
|
import BaseFileInput from './BaseFileInput';
|
||||||
import { BaseFileInputProps, formatTime } from './file-input-utils';
|
import { BaseFileInputProps, formatTime } from './file-input-utils';
|
||||||
|
import Cropper, { MediaSize, Point, Size, Area } from 'react-easy-crop';
|
||||||
|
|
||||||
interface VideoFileInputProps extends BaseFileInputProps {
|
interface VideoFileInputProps extends BaseFileInputProps {
|
||||||
showTrimControls?: boolean;
|
showTrimControls?: boolean;
|
||||||
onTrimChange?: (trimStart: number, trimEnd: number) => void;
|
onTrimChange?: (trimStart: number, trimEnd: number) => void;
|
||||||
trimStart?: number;
|
trimStart?: number;
|
||||||
trimEnd?: number;
|
trimEnd?: number;
|
||||||
|
showCropOverlay?: boolean;
|
||||||
|
cropPosition?: { x: number; y: number };
|
||||||
|
cropSize?: { width: number; height: number };
|
||||||
|
onCropChange?: (
|
||||||
|
position: { x: number; y: number },
|
||||||
|
size: { width: number; height: number }
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ToolVideoInput({
|
export default function ToolVideoInput({
|
||||||
@@ -17,11 +25,28 @@ export default function ToolVideoInput({
|
|||||||
onTrimChange,
|
onTrimChange,
|
||||||
trimStart = 0,
|
trimStart = 0,
|
||||||
trimEnd = 100,
|
trimEnd = 100,
|
||||||
|
showCropOverlay,
|
||||||
|
cropPosition = { x: 0, y: 0 },
|
||||||
|
cropSize = { width: 100, height: 100 },
|
||||||
|
onCropChange,
|
||||||
...props
|
...props
|
||||||
}: VideoFileInputProps) {
|
}: VideoFileInputProps) {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
let videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [videoDuration, setVideoDuration] = useState(0);
|
const [videoDuration, setVideoDuration] = useState(0);
|
||||||
|
const [videoWidth, setVideoWidth] = useState(0);
|
||||||
|
const [videoHeight, setVideoHeight] = useState(0);
|
||||||
|
|
||||||
|
const [crop, setCrop] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}>({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
});
|
||||||
const onVideoLoad = (e: React.SyntheticEvent<HTMLVideoElement>) => {
|
const onVideoLoad = (e: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||||
const duration = e.currentTarget.duration;
|
const duration = e.currentTarget.duration;
|
||||||
setVideoDuration(duration);
|
setVideoDuration(duration);
|
||||||
@@ -30,13 +55,59 @@ export default function ToolVideoInput({
|
|||||||
onTrimChange(0, duration);
|
onTrimChange(0, duration);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
cropPosition.x !== 0 ||
|
||||||
|
cropPosition.y !== 0 ||
|
||||||
|
cropSize.width !== 100 ||
|
||||||
|
cropSize.height !== 100
|
||||||
|
) {
|
||||||
|
setCrop({ ...cropPosition, ...cropSize });
|
||||||
|
}
|
||||||
|
}, [cropPosition, cropSize]);
|
||||||
|
|
||||||
|
const onCropMediaLoaded = (mediaSize: MediaSize) => {
|
||||||
|
const { width, height } = mediaSize;
|
||||||
|
setVideoWidth(width);
|
||||||
|
setVideoHeight(height);
|
||||||
|
if (!crop.width && !crop.height && onCropChange) {
|
||||||
|
const initialCrop = {
|
||||||
|
x: Math.floor(width / 4),
|
||||||
|
y: Math.floor(height / 4),
|
||||||
|
width: Math.floor(width / 2),
|
||||||
|
height: Math.floor(height / 2)
|
||||||
|
};
|
||||||
|
console.log('initialCrop', initialCrop);
|
||||||
|
setCrop(initialCrop);
|
||||||
|
|
||||||
|
onCropChange(
|
||||||
|
{ x: initialCrop.x, y: initialCrop.y },
|
||||||
|
{ width: initialCrop.width, height: initialCrop.height }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleTrimChange = (start: number, end: number) => {
|
const handleTrimChange = (start: number, end: number) => {
|
||||||
if (onTrimChange) {
|
if (onTrimChange) {
|
||||||
onTrimChange(start, end);
|
onTrimChange(start, end);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCropChange = (newCrop: Point) => {
|
||||||
|
setCrop((prevState) => ({ ...prevState, x: newCrop.x, y: newCrop.y }));
|
||||||
|
};
|
||||||
|
const handleCropSizeChange = (newCrop: Size) => {
|
||||||
|
setCrop((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
height: newCrop.height,
|
||||||
|
width: newCrop.width
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCropComplete = (crop: Area, croppedAreaPixels: Area) => {
|
||||||
|
if (onCropChange) {
|
||||||
|
onCropChange(croppedAreaPixels, croppedAreaPixels);
|
||||||
|
}
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<BaseFileInput {...props} type={'video'}>
|
<BaseFileInput {...props} type={'video'}>
|
||||||
{({ preview }) => (
|
{({ preview }) => (
|
||||||
@@ -51,16 +122,38 @@ export default function ToolVideoInput({
|
|||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<video
|
{showCropOverlay ? (
|
||||||
ref={videoRef}
|
<Cropper
|
||||||
src={preview}
|
setVideoRef={(ref) => {
|
||||||
style={{
|
videoRef = ref;
|
||||||
maxWidth: '100%',
|
}}
|
||||||
maxHeight: showTrimControls ? 'calc(100% - 50px)' : '100%'
|
video={preview}
|
||||||
}}
|
crop={crop}
|
||||||
onLoadedMetadata={onVideoLoad}
|
cropSize={crop}
|
||||||
controls={!showTrimControls}
|
onCropSizeChange={handleCropSizeChange}
|
||||||
/>
|
onCropChange={handleCropChange}
|
||||||
|
onCropComplete={handleCropComplete}
|
||||||
|
onMediaLoaded={onCropMediaLoaded}
|
||||||
|
initialCroppedAreaPercentages={{
|
||||||
|
height: 0.5,
|
||||||
|
width: 0.5,
|
||||||
|
x: 0.5,
|
||||||
|
y: 0.5
|
||||||
|
}}
|
||||||
|
// controls={!showTrimControls}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={preview}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: showTrimControls ? 'calc(100% - 50px)' : '100%'
|
||||||
|
}}
|
||||||
|
onLoadedMetadata={onVideoLoad}
|
||||||
|
controls={!showTrimControls}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{showTrimControls && videoDuration > 0 && (
|
{showTrimControls && videoDuration > 0 && (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { cropVideo } from './service';
|
|
||||||
|
|
||||||
// Mock FFmpeg
|
|
||||||
vi.mock('@ffmpeg/ffmpeg', () => {
|
|
||||||
return {
|
|
||||||
FFmpeg: vi.fn().mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
loaded: false,
|
|
||||||
load: vi.fn().mockResolvedValue(undefined),
|
|
||||||
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
||||||
exec: vi.fn().mockResolvedValue(undefined),
|
|
||||||
readFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3]))
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock fetchFile
|
|
||||||
vi.mock('@ffmpeg/util', () => {
|
|
||||||
return {
|
|
||||||
fetchFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3]))
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('crop-video', () => {
|
|
||||||
let mockFile: File;
|
|
||||||
let mockVideoInfo: { width: number; height: number };
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockFile = new File(['test'], 'test.mp4', { type: 'video/mp4' });
|
|
||||||
mockVideoInfo = { width: 1280, height: 720 };
|
|
||||||
|
|
||||||
// Reset global File constructor
|
|
||||||
global.File = vi.fn().mockImplementation((bits, name, options) => {
|
|
||||||
return new File(bits, name, options);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should crop a video with valid parameters', async () => {
|
|
||||||
const options = {
|
|
||||||
width: 640,
|
|
||||||
height: 360,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
maintainAspectRatio: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await cropVideo(mockFile, options, mockVideoInfo);
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(result.name).toContain('_cropped');
|
|
||||||
expect(result.type).toBe('video/mp4');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error if videoInfo is not provided', async () => {
|
|
||||||
const options = {
|
|
||||||
width: 640,
|
|
||||||
height: 360,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
maintainAspectRatio: false
|
|
||||||
};
|
|
||||||
|
|
||||||
await expect(cropVideo(mockFile, options, null)).rejects.toThrow(
|
|
||||||
'Video information is required'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should adjust crop dimensions to fit within video bounds', async () => {
|
|
||||||
const options = {
|
|
||||||
width: 2000, // Larger than video width
|
|
||||||
height: 1000, // Larger than video height
|
|
||||||
x: 500,
|
|
||||||
y: 500,
|
|
||||||
maintainAspectRatio: false
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await cropVideo(mockFile, options, mockVideoInfo);
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
// We can't test the actual crop dimensions since FFmpeg is mocked
|
|
||||||
// but we can verify the file was created
|
|
||||||
expect(result.name).toContain('_cropped');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Box, Checkbox, FormControlLabel } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import ToolFileResult from '@components/result/ToolFileResult';
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
import ToolContent from '@components/ToolContent';
|
import ToolContent from '@components/ToolContent';
|
||||||
import { ToolComponentProps } from '@tools/defineTool';
|
import { ToolComponentProps } from '@tools/defineTool';
|
||||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
import { GetGroupsType, UpdateField } from '@components/options/ToolOptions';
|
||||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||||
import { updateNumberField } from '@utils/string';
|
import { updateNumberField } from '@utils/string';
|
||||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||||
@@ -19,8 +19,7 @@ const initialValues: InitialValuesType = {
|
|||||||
width: 640,
|
width: 640,
|
||||||
height: 360,
|
height: 360,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0
|
||||||
maintainAspectRatio: true
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
@@ -46,14 +45,12 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
||||||
|
|
||||||
// Get video dimensions when a video is loaded
|
// Get video dimensions when a video is loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (input) {
|
if (input) {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.onloadedmetadata = () => {
|
video.onloadedmetadata = () => {
|
||||||
console.log('loadedmetadata', video.videoWidth, video.videoHeight);
|
|
||||||
setVideoInfo({
|
setVideoInfo({
|
||||||
width: video.videoWidth,
|
width: video.videoWidth,
|
||||||
height: video.videoHeight
|
height: video.videoHeight
|
||||||
@@ -133,8 +130,7 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
|||||||
|
|
||||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||||
values,
|
values,
|
||||||
updateField,
|
updateField
|
||||||
setFieldValue
|
|
||||||
}) => [
|
}) => [
|
||||||
{
|
{
|
||||||
title: 'Crop Settings',
|
title: 'Crop Settings',
|
||||||
@@ -172,34 +168,48 @@ export default function CropVideo({ title }: ToolComponentProps) {
|
|||||||
label={'Y Position (pixels)'}
|
label={'Y Position (pixels)'}
|
||||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||||
/>
|
/>
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
checked={values.maintainAspectRatio}
|
|
||||||
onChange={(e) => {
|
|
||||||
setFieldValue('maintainAspectRatio', e.target.checked);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Maintain aspect ratio"
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
const handleCropChange =
|
||||||
|
(values: InitialValuesType, updateField: UpdateField<InitialValuesType>) =>
|
||||||
|
(
|
||||||
|
position: { x: number; y: number },
|
||||||
|
size: { width: number; height: number }
|
||||||
|
) => {
|
||||||
|
console.log('Crop position:', position, size);
|
||||||
|
updateField('x', position.x);
|
||||||
|
updateField('y', position.y);
|
||||||
|
updateField('width', size.width);
|
||||||
|
updateField('height', size.height);
|
||||||
|
};
|
||||||
|
const renderCustomInput = (
|
||||||
|
values: InitialValuesType,
|
||||||
|
updateField: UpdateField<InitialValuesType>
|
||||||
|
) => (
|
||||||
|
<ToolVideoInput
|
||||||
|
value={input}
|
||||||
|
onChange={setInput}
|
||||||
|
accept={['video/mp4', 'video/webm', 'video/ogg']}
|
||||||
|
title={'Input Video'}
|
||||||
|
showCropOverlay={!!input}
|
||||||
|
cropPosition={{
|
||||||
|
x: parseInt(values.x || '0'),
|
||||||
|
y: parseInt(values.y || '0')
|
||||||
|
}}
|
||||||
|
cropSize={{
|
||||||
|
width: parseInt(values.width || '100'),
|
||||||
|
height: parseInt(values.height || '100')
|
||||||
|
}}
|
||||||
|
onCropChange={handleCropChange(values, updateField)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<ToolContent
|
<ToolContent
|
||||||
title={title}
|
title={title}
|
||||||
input={input}
|
input={input}
|
||||||
inputComponent={
|
renderCustomInput={renderCustomInput}
|
||||||
<ToolVideoInput
|
|
||||||
value={input}
|
|
||||||
onChange={setInput}
|
|
||||||
accept={['video/mp4', 'video/webm', 'video/ogg']}
|
|
||||||
title={'Input Video'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
resultComponent={
|
resultComponent={
|
||||||
<ToolFileResult
|
<ToolFileResult
|
||||||
title={'Cropped Video'}
|
title={'Cropped Video'}
|
||||||
|
|||||||
@@ -3,5 +3,4 @@ export type InitialValuesType = {
|
|||||||
height: number;
|
height: number;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
maintainAspectRatio: boolean;
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user