mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-19 05:59:34 +02:00
Merge pull request #56 from iib0011/refactor-file-input
refactor: file inputs
This commit is contained in:
19
.idea/workspace.xml
generated
19
.idea/workspace.xml
generated
@@ -5,11 +5,18 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: trim video">
|
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: trim video">
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/components/input/BaseFileInput.tsx" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/components/input/ToolImageInput.tsx" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/components/input/ToolVideoInput.tsx" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/src/components/input/file-input-utils.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$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/change-colors-in-png/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/png/change-colors-in-png/index.tsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/csv/csv-to-xml/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/csv/csv-to-xml/index.tsx" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/change-opacity/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/png/change-opacity/index.tsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/csv/csv-to-xml/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/csv/csv-to-xml/meta.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/compress-png/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/png/compress-png/index.tsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/csv/csv-to-xml/service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/csv/csv-to-xml/service.ts" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/convert-jgp-to-png/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/png/convert-jgp-to-png/index.tsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/create-transparent/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/png/create-transparent/index.tsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/crop/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/png/crop/index.tsx" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/trim/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/trim/index.tsx" 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" />
|
||||||
@@ -26,7 +33,7 @@
|
|||||||
<option name="PUSH_AUTO_UPDATE" value="true" />
|
<option name="PUSH_AUTO_UPDATE" value="true" />
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="chesterkxng" />
|
<entry key="$PROJECT_DIR$" value="main" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
@@ -131,7 +138,7 @@
|
|||||||
"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",
|
||||||
"git-widget-placeholder": "#53 on fork/ARRY7686/csvToXml",
|
"git-widget-placeholder": "refactor-file-input",
|
||||||
"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/pages/tools/list/duplicate/index.tsx",
|
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src/pages/tools/list/duplicate/index.tsx",
|
||||||
|
147
src/components/input/BaseFileInput.tsx
Normal file
147
src/components/input/BaseFileInput.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import React, { ReactNode, useContext, useEffect } from 'react';
|
||||||
|
import { Box, useTheme } from '@mui/material';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import InputHeader from '../InputHeader';
|
||||||
|
import InputFooter from './InputFooter';
|
||||||
|
import {
|
||||||
|
BaseFileInputProps,
|
||||||
|
createObjectURL,
|
||||||
|
revokeObjectURL
|
||||||
|
} from './file-input-utils';
|
||||||
|
import { globalInputHeight } from '../../config/uiConfig';
|
||||||
|
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||||
|
import greyPattern from '@assets/grey-pattern.png';
|
||||||
|
|
||||||
|
interface BaseFileInputComponentProps extends BaseFileInputProps {
|
||||||
|
children: (props: { preview: string | undefined }) => ReactNode;
|
||||||
|
type: 'image' | 'video' | 'audio';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BaseFileInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
accept,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
type
|
||||||
|
}: BaseFileInputComponentProps) {
|
||||||
|
const [preview, setPreview] = React.useState<string | null>(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value) {
|
||||||
|
const objectUrl = createObjectURL(value);
|
||||||
|
setPreview(objectUrl);
|
||||||
|
|
||||||
|
return () => revokeObjectURL(objectUrl);
|
||||||
|
} else {
|
||||||
|
setPreview(null);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (file) onChange(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
const handleCopy = () => {
|
||||||
|
if (value) {
|
||||||
|
const blob = new Blob([value], { type: value.type });
|
||||||
|
const clipboardItem = new ClipboardItem({ [value.type]: blob });
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.write([clipboardItem])
|
||||||
|
.then(() => showSnackBar('File copied', 'success'))
|
||||||
|
.catch((err) => {
|
||||||
|
showSnackBar('Failed to copy: ' + err, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePaste = (event: ClipboardEvent) => {
|
||||||
|
const clipboardItems = event.clipboardData?.items ?? [];
|
||||||
|
const item = clipboardItems[0];
|
||||||
|
if (
|
||||||
|
item &&
|
||||||
|
(item.type.includes('image') || item.type.includes('video'))
|
||||||
|
) {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file) onChange(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('paste', handlePaste);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('paste', handlePaste);
|
||||||
|
};
|
||||||
|
}, [onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<InputHeader
|
||||||
|
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: globalInputHeight,
|
||||||
|
border: preview ? 0 : 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: '5',
|
||||||
|
bgcolor: 'white',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{preview ? (
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundImage: `url(${greyPattern})`,
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children({ preview })}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
onClick={handleImportClick}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: 5,
|
||||||
|
height: '100%',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography color={theme.palette.grey['600']}>
|
||||||
|
Click here to select a {type} from your device, press Ctrl+V to
|
||||||
|
use a {type} from your clipboard, drag and drop a file from
|
||||||
|
desktop
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
type="file"
|
||||||
|
accept={accept.join(',')}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
142
src/components/input/ToolImageInput.tsx
Normal file
142
src/components/input/ToolImageInput.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import ReactCrop, { Crop, PixelCrop } from 'react-image-crop';
|
||||||
|
import 'react-image-crop/dist/ReactCrop.css';
|
||||||
|
import BaseFileInput from './BaseFileInput';
|
||||||
|
import { BaseFileInputProps } from './file-input-utils';
|
||||||
|
import { globalInputHeight } from '../../config/uiConfig';
|
||||||
|
|
||||||
|
interface ImageFileInputProps extends BaseFileInputProps {
|
||||||
|
showCropOverlay?: boolean;
|
||||||
|
cropShape?: 'rectangular' | 'circular';
|
||||||
|
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 ToolImageInput({
|
||||||
|
showCropOverlay = false,
|
||||||
|
cropShape = 'rectangular',
|
||||||
|
cropPosition = { x: 0, y: 0 },
|
||||||
|
cropSize = { width: 100, height: 100 },
|
||||||
|
onCropChange,
|
||||||
|
...props
|
||||||
|
}: ImageFileInputProps) {
|
||||||
|
const imageRef = useRef<HTMLImageElement>(null);
|
||||||
|
const [imgWidth, setImgWidth] = useState(0);
|
||||||
|
const [imgHeight, setImgHeight] = useState(0);
|
||||||
|
|
||||||
|
const [crop, setCrop] = useState<Crop>({
|
||||||
|
unit: 'px',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const RATIO = imageRef.current ? imgWidth / imageRef.current.width : 1;
|
||||||
|
|
||||||
|
const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
||||||
|
const { naturalWidth: width, naturalHeight: height } = e.currentTarget;
|
||||||
|
setImgWidth(width);
|
||||||
|
setImgHeight(height);
|
||||||
|
|
||||||
|
if (!crop.width && !crop.height && onCropChange) {
|
||||||
|
const initialCrop: Crop = {
|
||||||
|
unit: 'px',
|
||||||
|
x: Math.floor(width / 4),
|
||||||
|
y: Math.floor(height / 4),
|
||||||
|
width: Math.floor(width / 2),
|
||||||
|
height: Math.floor(height / 2)
|
||||||
|
};
|
||||||
|
|
||||||
|
setCrop(initialCrop);
|
||||||
|
|
||||||
|
onCropChange(
|
||||||
|
{ x: initialCrop.x, y: initialCrop.y },
|
||||||
|
{ width: initialCrop.width, height: initialCrop.height }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
imgWidth &&
|
||||||
|
imgHeight &&
|
||||||
|
(cropPosition.x !== 0 ||
|
||||||
|
cropPosition.y !== 0 ||
|
||||||
|
cropSize.width !== 100 ||
|
||||||
|
cropSize.height !== 100)
|
||||||
|
) {
|
||||||
|
setCrop({
|
||||||
|
unit: 'px',
|
||||||
|
x: cropPosition.x / RATIO,
|
||||||
|
y: cropPosition.y / RATIO,
|
||||||
|
width: cropSize.width / RATIO,
|
||||||
|
height: cropSize.height / RATIO
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [cropPosition, cropSize, imgWidth, imgHeight, RATIO]);
|
||||||
|
|
||||||
|
const handleCropChange = (newCrop: Crop) => {
|
||||||
|
setCrop(newCrop);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCropComplete = (crop: PixelCrop) => {
|
||||||
|
if (onCropChange) {
|
||||||
|
onCropChange(
|
||||||
|
{ x: Math.round(crop.x * RATIO), y: Math.round(crop.y * RATIO) },
|
||||||
|
{
|
||||||
|
width: Math.round(crop.width * RATIO),
|
||||||
|
height: Math.round(crop.height * RATIO)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseFileInput {...props} type={'image'}>
|
||||||
|
{({ preview }) => (
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showCropOverlay ? (
|
||||||
|
<ReactCrop
|
||||||
|
crop={crop}
|
||||||
|
onChange={handleCropChange}
|
||||||
|
onComplete={handleCropComplete}
|
||||||
|
circularCrop={cropShape === 'circular'}
|
||||||
|
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
ref={imageRef}
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
|
||||||
|
onLoad={onImageLoad}
|
||||||
|
/>
|
||||||
|
</ReactCrop>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
ref={imageRef}
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
|
||||||
|
onLoad={onImageLoad}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</BaseFileInput>
|
||||||
|
);
|
||||||
|
}
|
121
src/components/input/ToolVideoInput.tsx
Normal file
121
src/components/input/ToolVideoInput.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
import Slider from 'rc-slider';
|
||||||
|
import 'rc-slider/assets/index.css';
|
||||||
|
import BaseFileInput from './BaseFileInput';
|
||||||
|
import { BaseFileInputProps, formatTime } from './file-input-utils';
|
||||||
|
|
||||||
|
interface VideoFileInputProps extends BaseFileInputProps {
|
||||||
|
showTrimControls?: boolean;
|
||||||
|
onTrimChange?: (trimStart: number, trimEnd: number) => void;
|
||||||
|
trimStart?: number;
|
||||||
|
trimEnd?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolVideoInput({
|
||||||
|
showTrimControls = false,
|
||||||
|
onTrimChange,
|
||||||
|
trimStart = 0,
|
||||||
|
trimEnd = 100,
|
||||||
|
...props
|
||||||
|
}: VideoFileInputProps) {
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
const [videoDuration, setVideoDuration] = useState(0);
|
||||||
|
|
||||||
|
const onVideoLoad = (e: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||||
|
const duration = e.currentTarget.duration;
|
||||||
|
setVideoDuration(duration);
|
||||||
|
|
||||||
|
if (onTrimChange && trimStart === 0 && trimEnd === 100) {
|
||||||
|
onTrimChange(0, duration);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTrimChange = (start: number, end: number) => {
|
||||||
|
if (onTrimChange) {
|
||||||
|
onTrimChange(start, end);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseFileInput {...props} type={'video'}>
|
||||||
|
{({ preview }) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={preview}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: showTrimControls ? 'calc(100% - 50px)' : '100%'
|
||||||
|
}}
|
||||||
|
onLoadedMetadata={onVideoLoad}
|
||||||
|
controls={!showTrimControls}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showTrimControls && videoDuration > 0 && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px 20px',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
color: 'white',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">
|
||||||
|
Start: {formatTime(trimStart || 0)}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption">
|
||||||
|
End: {formatTime(trimEnd || videoDuration)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<div
|
||||||
|
className="range-slider-container"
|
||||||
|
style={{ margin: '20px 0', width: '100%' }}
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
range
|
||||||
|
min={0}
|
||||||
|
max={videoDuration}
|
||||||
|
step={0.1}
|
||||||
|
value={[trimStart || 0, trimEnd || videoDuration]}
|
||||||
|
onChange={(values) => {
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
handleTrimChange(values[0], values[1]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
allowCross={false}
|
||||||
|
pushable={0.1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</BaseFileInput>
|
||||||
|
);
|
||||||
|
}
|
22
src/components/input/file-input-utils.ts
Normal file
22
src/components/input/file-input-utils.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export interface BaseFileInputProps {
|
||||||
|
value: File | null;
|
||||||
|
onChange: (file: File) => void;
|
||||||
|
accept: string[];
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatTime = (seconds: number): string => {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remainingSeconds = Math.floor(seconds % 60);
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createObjectURL = (file: File): string => {
|
||||||
|
return URL.createObjectURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const revokeObjectURL = (url: string): void => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
@@ -1,16 +1,15 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import ToolFileInput from '@components/input/ToolFileInput';
|
|
||||||
import ToolFileResult from '@components/result/ToolFileResult';
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
import ToolOptions, { GetGroupsType } from '@components/options/ToolOptions';
|
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||||
import ColorSelector from '@components/options/ColorSelector';
|
import ColorSelector from '@components/options/ColorSelector';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||||
import ToolInputAndResult from '@components/ToolInputAndResult';
|
|
||||||
import { areColorsSimilar } from 'utils/color';
|
import { areColorsSimilar } from 'utils/color';
|
||||||
import ToolContent from '@components/ToolContent';
|
import ToolContent from '@components/ToolContent';
|
||||||
import { ToolComponentProps } from '@tools/defineTool';
|
import { ToolComponentProps } from '@tools/defineTool';
|
||||||
|
import ToolImageInput from '@components/input/ToolImageInput';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
fromColor: 'white',
|
fromColor: 'white',
|
||||||
@@ -125,7 +124,7 @@ export default function ChangeColorsInPng({ title }: ToolComponentProps) {
|
|||||||
input={input}
|
input={input}
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
inputComponent={
|
inputComponent={
|
||||||
<ToolFileInput
|
<ToolImageInput
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
accept={['image/png']}
|
accept={['image/png']}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import ToolFileInput from '@components/input/ToolFileInput';
|
import ToolImageInput from '@components/input/ToolImageInput';
|
||||||
import ToolFileResult from '@components/result/ToolFileResult';
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
import { changeOpacity } from './service';
|
import { changeOpacity } from './service';
|
||||||
import ToolContent from '@components/ToolContent';
|
import ToolContent from '@components/ToolContent';
|
||||||
@@ -94,7 +94,7 @@ export default function ChangeOpacity({ title }: ToolComponentProps) {
|
|||||||
title={title}
|
title={title}
|
||||||
input={input}
|
input={input}
|
||||||
inputComponent={
|
inputComponent={
|
||||||
<ToolFileInput
|
<ToolImageInput
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
accept={['image/png']}
|
accept={['image/png']}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import ToolFileInput from '@components/input/ToolFileInput';
|
import ToolImageInput from '@components/input/ToolImageInput';
|
||||||
import ToolFileResult from '@components/result/ToolFileResult';
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||||
import imageCompression from 'browser-image-compression';
|
import imageCompression from 'browser-image-compression';
|
||||||
@@ -56,7 +56,7 @@ export default function ChangeColorsInPng({ title }: ToolComponentProps) {
|
|||||||
title={title}
|
title={title}
|
||||||
input={input}
|
input={input}
|
||||||
inputComponent={
|
inputComponent={
|
||||||
<ToolFileInput
|
<ToolImageInput
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
accept={['image/png']}
|
accept={['image/png']}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import ToolFileInput from 'components/input/ToolFileInput';
|
import ToolImageInput from 'components/input/ToolImageInput';
|
||||||
import CheckboxWithDesc from 'components/options/CheckboxWithDesc';
|
import CheckboxWithDesc from 'components/options/CheckboxWithDesc';
|
||||||
import ColorSelector from 'components/options/ColorSelector';
|
import ColorSelector from 'components/options/ColorSelector';
|
||||||
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from 'components/options/TextFieldWithDesc';
|
||||||
@@ -101,7 +101,7 @@ export default function ConvertJgpToPng({ title }: ToolComponentProps) {
|
|||||||
title={title}
|
title={title}
|
||||||
input={input}
|
input={input}
|
||||||
inputComponent={
|
inputComponent={
|
||||||
<ToolFileInput
|
<ToolImageInput
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
accept={['image/jpeg']}
|
accept={['image/jpeg']}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import ToolFileInput from '@components/input/ToolFileInput';
|
import ToolImageInput from '@components/input/ToolImageInput';
|
||||||
import ToolFileResult from '@components/result/ToolFileResult';
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
import ColorSelector from '@components/options/ColorSelector';
|
import ColorSelector from '@components/options/ColorSelector';
|
||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
@@ -109,7 +109,7 @@ export default function CreateTransparent({ title }: ToolComponentProps) {
|
|||||||
<ToolContent
|
<ToolContent
|
||||||
title={title}
|
title={title}
|
||||||
inputComponent={
|
inputComponent={
|
||||||
<ToolFileInput
|
<ToolImageInput
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
accept={['image/png']}
|
accept={['image/png']}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import ToolFileInput from '@components/input/ToolFileInput';
|
import ToolImageInput from '@components/input/ToolImageInput';
|
||||||
import ToolFileResult from '@components/result/ToolFileResult';
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
import { GetGroupsType, UpdateField } from '@components/options/ToolOptions';
|
import { GetGroupsType, UpdateField } from '@components/options/ToolOptions';
|
||||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||||
@@ -197,7 +197,7 @@ export default function CropPng({ title }: ToolComponentProps) {
|
|||||||
values: InitialValuesType,
|
values: InitialValuesType,
|
||||||
updateField: UpdateField<InitialValuesType>
|
updateField: UpdateField<InitialValuesType>
|
||||||
) => (
|
) => (
|
||||||
<ToolFileInput
|
<ToolImageInput
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
accept={['image/png']}
|
accept={['image/png']}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import ToolFileInput from '@components/input/ToolFileInput';
|
|
||||||
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';
|
||||||
@@ -11,6 +10,7 @@ import { updateNumberField } from '@utils/string';
|
|||||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||||
import { fetchFile } from '@ffmpeg/util';
|
import { fetchFile } from '@ffmpeg/util';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||||
|
|
||||||
const ffmpeg = new FFmpeg();
|
const ffmpeg = new FFmpeg();
|
||||||
|
|
||||||
@@ -35,14 +35,16 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
|||||||
optionsValues: typeof initialValues,
|
optionsValues: typeof initialValues,
|
||||||
input: File | null
|
input: File | null
|
||||||
) => {
|
) => {
|
||||||
console.log('compute', optionsValues, input);
|
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
|
|
||||||
const { trimStart, trimEnd } = optionsValues;
|
const { trimStart, trimEnd } = optionsValues;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!ffmpeg.loaded) {
|
if (!ffmpeg.loaded) {
|
||||||
await ffmpeg.load();
|
await ffmpeg.load({
|
||||||
|
wasmURL:
|
||||||
|
'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputName = 'input.mp4';
|
const inputName = 'input.mp4';
|
||||||
@@ -111,12 +113,11 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
|||||||
input={input}
|
input={input}
|
||||||
renderCustomInput={({ trimStart, trimEnd }, setFieldValue) => {
|
renderCustomInput={({ trimStart, trimEnd }, setFieldValue) => {
|
||||||
return (
|
return (
|
||||||
<ToolFileInput
|
<ToolVideoInput
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
accept={['video/mp4', 'video/webm', 'video/ogg']}
|
accept={['video/mp4', 'video/webm', 'video/ogg']}
|
||||||
title={'Input Video'}
|
title={'Input Video'}
|
||||||
type="video"
|
|
||||||
showTrimControls={true}
|
showTrimControls={true}
|
||||||
onTrimChange={(trimStart, trimEnd) => {
|
onTrimChange={(trimStart, trimEnd) => {
|
||||||
setFieldValue('trimStart', trimStart);
|
setFieldValue('trimStart', trimStart);
|
||||||
|
Reference in New Issue
Block a user