mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge branch 'main' into pr/Srivarshan-T/216
This commit is contained in:
@@ -127,7 +127,10 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
></iframe>,
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.open('https://buymeacoffee.com/iib0011', '_blank');
|
||||
window.open(
|
||||
'https://drive.google.com/file/d/1-r9-rDYnDJic9dnDywKTAsueehIAVp5F/view?usp=sharing',
|
||||
'_blank'
|
||||
);
|
||||
}}
|
||||
sx={{ borderRadius: '100px' }}
|
||||
variant={'contained'}
|
||||
@@ -135,11 +138,11 @@ const Navbar: React.FC<NavbarProps> = ({
|
||||
<Icon
|
||||
style={{ cursor: 'pointer' }}
|
||||
fontSize={25}
|
||||
icon={'mdi:heart-outline'}
|
||||
icon={'hugeicons:job-search'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t('navbar.buyMeACoffee')}
|
||||
{t('navbar.hireMe')}
|
||||
</Button>
|
||||
];
|
||||
const drawerList = (
|
||||
|
||||
@@ -41,7 +41,7 @@ export async function changeAudioSpeed(
|
||||
const outputName = `output.${outputFormat}`;
|
||||
await ffmpeg.writeFile(fileName, await fetchFile(input));
|
||||
const audioFilter = computeAudioFilter(newSpeed);
|
||||
let args = ['-i', fileName, '-filter:a', audioFilter];
|
||||
const args = ['-i', fileName, '-filter:a', audioFilter];
|
||||
if (outputFormat === 'mp3') {
|
||||
args.push('-b:a', '192k', '-f', 'mp3', outputName);
|
||||
} else if (outputFormat === 'aac') {
|
||||
@@ -64,7 +64,7 @@ export async function changeAudioSpeed(
|
||||
let mimeType = 'audio/mp3';
|
||||
if (outputFormat === 'aac') mimeType = 'audio/aac';
|
||||
if (outputFormat === 'wav') mimeType = 'audio/wav';
|
||||
const blob = new Blob([data], { type: mimeType });
|
||||
const blob = new Blob([data as any], { type: mimeType });
|
||||
const newFile = new File(
|
||||
[blob],
|
||||
fileName.replace(/\.[^/.]+$/, `-${newSpeed}x.${outputFormat}`),
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function extractAudioFromVideo(
|
||||
|
||||
return new File(
|
||||
[
|
||||
new Blob([extractedAudio], {
|
||||
new Blob([extractedAudio as any], {
|
||||
type: `audio/${configuredOutputAudioFormat}`
|
||||
})
|
||||
],
|
||||
|
||||
@@ -105,7 +105,7 @@ export async function mergeAudioFiles(
|
||||
|
||||
return new File(
|
||||
[
|
||||
new Blob([mergedAudio], {
|
||||
new Blob([mergedAudio as any], {
|
||||
type: mimeType
|
||||
})
|
||||
],
|
||||
|
||||
@@ -98,7 +98,7 @@ export async function trimAudio(
|
||||
|
||||
return new File(
|
||||
[
|
||||
new Blob([trimmedAudio], {
|
||||
new Blob([trimmedAudio as any], {
|
||||
type: mimeType
|
||||
})
|
||||
],
|
||||
|
||||
@@ -40,7 +40,7 @@ const initialValues: InitialValuesType = {
|
||||
// WiFi
|
||||
wifiSsid: '',
|
||||
wifiPassword: '',
|
||||
wifiEncryption: 'WPA/WPA2',
|
||||
wifiEncryption: 'WPA',
|
||||
|
||||
// vCard
|
||||
vCardName: '',
|
||||
@@ -353,7 +353,7 @@ export default function QRCodeGenerator({ title }: ToolComponentProps) {
|
||||
label="Encryption Type"
|
||||
margin="normal"
|
||||
>
|
||||
<MenuItem value="WPA/WPA2">WPA/WPA2</MenuItem>
|
||||
<MenuItem value="WPA">WPA</MenuItem>
|
||||
<MenuItem value="WEP">WEP</MenuItem>
|
||||
<MenuItem value="None">None</MenuItem>
|
||||
</TextField>
|
||||
|
||||
@@ -7,7 +7,7 @@ export type QRCodeType =
|
||||
| 'WiFi'
|
||||
| 'vCard';
|
||||
|
||||
export type WifiEncryptionType = 'WPA/WPA2' | 'WEP' | 'None';
|
||||
export type WifiEncryptionType = 'WPA' | 'WEP' | 'None';
|
||||
|
||||
export interface InitialValuesType {
|
||||
qrCodeType: QRCodeType;
|
||||
|
||||
@@ -148,7 +148,7 @@ export const processImage = async (
|
||||
const data = await ffmpeg.readFile('output.gif');
|
||||
|
||||
// Create a new File object
|
||||
return new File([data], file.name, { type: 'image/gif' });
|
||||
return new File([data as any], file.name, { type: 'image/gif' });
|
||||
} catch (error) {
|
||||
console.error('Error processing GIF with FFmpeg:', error);
|
||||
// Fall back to canvas method if FFmpeg processing fails
|
||||
|
||||
@@ -66,7 +66,7 @@ export const processImage = async (
|
||||
|
||||
// Read the output file
|
||||
const data = await ffmpeg.readFile('output.' + file.name.split('.').pop());
|
||||
return new File([data], file.name, { type: file.type });
|
||||
return new File([data as any], file.name, { type: file.type });
|
||||
} catch (error) {
|
||||
console.error('Error processing image:', error);
|
||||
return null;
|
||||
|
||||
@@ -3,6 +3,7 @@ import voltageDropInWire from './voltageDropInWire';
|
||||
import sphereArea from './sphereArea';
|
||||
import sphereVolume from './sphereVolume';
|
||||
import slackline from './slackline';
|
||||
|
||||
export default [
|
||||
ohmslaw,
|
||||
voltageDropInWire,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import ConvertToPdf from './index';
|
||||
import { vi } from 'vitest';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
describe('ConvertToPdf', () => {
|
||||
it('renders with default state values (full, portrait hidden, no scale shown)', () => {
|
||||
render(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
expect(screen.getByLabelText(/Full Size \(Same as Image\)/i)).toBeChecked();
|
||||
|
||||
expect(screen.queryByLabelText(/A4 Page/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/Portrait/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Scale image:/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches to A4 page type and shows orientation and scale', () => {
|
||||
render(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
const a4Option = screen.getByLabelText(/A4 Page/i);
|
||||
fireEvent.click(a4Option);
|
||||
expect(a4Option).toBeChecked();
|
||||
|
||||
expect(screen.getByLabelText(/Portrait/i)).toBeChecked();
|
||||
expect(screen.getByText(/Scale image:\s*100%/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates scale when slider moves (after switching to A4)', () => {
|
||||
render(<ConvertToPdf title="Test PDF" />);
|
||||
|
||||
fireEvent.click(screen.getByLabelText(/A4 Page/i));
|
||||
|
||||
const slider = screen.getByRole('slider');
|
||||
fireEvent.change(slider, { target: { value: 80 } });
|
||||
|
||||
expect(screen.getByText(/Scale image:\s*80%/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
159
src/pages/tools/pdf/convert-to-pdf/index.tsx
Normal file
159
src/pages/tools/pdf/convert-to-pdf/index.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
Box,
|
||||
Slider,
|
||||
Typography,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolImageInput from 'components/input/ToolImageInput';
|
||||
import ToolFileResult from 'components/result/ToolFileResult';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { FormValues, Orientation, PageType, initialValues } from './types';
|
||||
import { buildPdf } from './service';
|
||||
|
||||
const initialFormValues: FormValues = initialValues;
|
||||
|
||||
export default function ConvertToPdf({ title }: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [imageSize, setImageSize] = useState<{
|
||||
widthMm: number;
|
||||
heightMm: number;
|
||||
widthPx: number;
|
||||
heightPx: number;
|
||||
} | null>(null);
|
||||
|
||||
const compute = async (values: FormValues) => {
|
||||
if (!input) return;
|
||||
const { pdfFile, imageSize } = await buildPdf({
|
||||
file: input,
|
||||
pageType: values.pageType,
|
||||
orientation: values.orientation,
|
||||
scale: values.scale
|
||||
});
|
||||
setResult(pdfFile);
|
||||
setImageSize(imageSize);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolContent<FormValues, File | null>
|
||||
title={title}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
initialValues={initialFormValues}
|
||||
compute={compute}
|
||||
inputComponent={
|
||||
<Box>
|
||||
<ToolImageInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={[
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/tiff',
|
||||
'image/gif',
|
||||
'image/heic',
|
||||
'image/heif',
|
||||
'image/x-adobe-dng',
|
||||
'image/x-canon-cr2',
|
||||
'image/x-nikon-nef',
|
||||
'image/x-sony-arw',
|
||||
'image/vnd.adobe.photoshop'
|
||||
]}
|
||||
title="Input Image"
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
getGroups={({ values, updateField }) => {
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
component: (
|
||||
<Stack spacing={4}>
|
||||
<Box>
|
||||
<Typography variant="h6">PDF Type</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
value={values.pageType}
|
||||
onChange={(e) =>
|
||||
updateField('pageType', e.target.value as PageType)
|
||||
}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="full"
|
||||
control={<Radio />}
|
||||
label="Full Size (Same as Image)"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="a4"
|
||||
control={<Radio />}
|
||||
label="A4 Page"
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
{values.pageType === 'full' && imageSize && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Image size: {imageSize.widthMm.toFixed(1)} ×{' '}
|
||||
{imageSize.heightMm.toFixed(1)} mm ({imageSize.widthPx} ×{' '}
|
||||
{imageSize.heightPx} px)
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{values.pageType === 'a4' && (
|
||||
<Box>
|
||||
<Typography variant="h6">Orientation</Typography>
|
||||
<RadioGroup
|
||||
row
|
||||
value={values.orientation}
|
||||
onChange={(e) =>
|
||||
updateField(
|
||||
'orientation',
|
||||
e.target.value as Orientation
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="portrait"
|
||||
control={<Radio />}
|
||||
label="Portrait (Vertical)"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="landscape"
|
||||
control={<Radio />}
|
||||
label="Landscape (Horizontal)"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{values.pageType === 'a4' && (
|
||||
<Box>
|
||||
<Typography variant="h6">Scale</Typography>
|
||||
<Typography>Scale image: {values.scale}%</Typography>
|
||||
<Slider
|
||||
value={values.scale}
|
||||
onChange={(_, v) => updateField('scale', v as number)}
|
||||
min={10}
|
||||
max={100}
|
||||
step={1}
|
||||
valueLabelDisplay="auto"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
] as const;
|
||||
}}
|
||||
resultComponent={
|
||||
<ToolFileResult title="Output PDF" value={result} extension="pdf" />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
32
src/pages/tools/pdf/convert-to-pdf/meta.ts
Normal file
32
src/pages/tools/pdf/convert-to-pdf/meta.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('pdf', {
|
||||
i18n: {
|
||||
name: 'pdf:convertToPdf.title',
|
||||
description: 'pdf:convertToPdf.description',
|
||||
shortDescription: 'pdf:convertToPdf.shortDescription'
|
||||
},
|
||||
|
||||
path: 'convert-to-pdf',
|
||||
icon: 'ph:file-pdf-thin',
|
||||
|
||||
keywords: [
|
||||
'convert',
|
||||
'pdf',
|
||||
'image',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'png',
|
||||
'gif',
|
||||
'tiff',
|
||||
'webp',
|
||||
'heic',
|
||||
'raw',
|
||||
'psd',
|
||||
'svg',
|
||||
'quality',
|
||||
'compression'
|
||||
],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
80
src/pages/tools/pdf/convert-to-pdf/service.ts
Normal file
80
src/pages/tools/pdf/convert-to-pdf/service.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import jsPDF from 'jspdf';
|
||||
import { Orientation, PageType, ImageSize } from './types';
|
||||
|
||||
export interface ComputeOptions {
|
||||
file: File;
|
||||
pageType: PageType;
|
||||
orientation: Orientation;
|
||||
scale: number; // 10..100 (only applied for A4)
|
||||
}
|
||||
|
||||
export interface ComputeResult {
|
||||
pdfFile: File;
|
||||
imageSize: ImageSize;
|
||||
}
|
||||
|
||||
export async function buildPdf({
|
||||
file,
|
||||
pageType,
|
||||
orientation,
|
||||
scale
|
||||
}: ComputeOptions): Promise<ComputeResult> {
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(file);
|
||||
|
||||
try {
|
||||
await img.decode();
|
||||
|
||||
const pxToMm = (px: number) => px * 0.264583;
|
||||
const imgWidthMm = pxToMm(img.width);
|
||||
const imgHeightMm = pxToMm(img.height);
|
||||
|
||||
const pdf =
|
||||
pageType === 'full'
|
||||
? new jsPDF({
|
||||
orientation: imgWidthMm > imgHeightMm ? 'landscape' : 'portrait',
|
||||
unit: 'mm',
|
||||
format: [imgWidthMm, imgHeightMm]
|
||||
})
|
||||
: new jsPDF({
|
||||
orientation,
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
pdf.setDisplayMode('fullwidth');
|
||||
|
||||
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||
|
||||
const widthRatio = pageWidth / img.width;
|
||||
const heightRatio = pageHeight / img.height;
|
||||
const fitScale = Math.min(widthRatio, heightRatio);
|
||||
|
||||
const finalWidth =
|
||||
pageType === 'full' ? pageWidth : img.width * fitScale * (scale / 100);
|
||||
|
||||
const finalHeight =
|
||||
pageType === 'full' ? pageHeight : img.height * fitScale * (scale / 100);
|
||||
|
||||
const x = pageType === 'full' ? 0 : (pageWidth - finalWidth) / 2;
|
||||
const y = pageType === 'full' ? 0 : (pageHeight - finalHeight) / 2;
|
||||
|
||||
pdf.addImage(img, 'JPEG', x, y, finalWidth, finalHeight);
|
||||
|
||||
const blob = pdf.output('blob');
|
||||
const fileName = file.name.replace(/\.[^/.]+$/, '') + '.pdf';
|
||||
|
||||
return {
|
||||
pdfFile: new File([blob], fileName, { type: 'application/pdf' }),
|
||||
imageSize: {
|
||||
widthMm: imgWidthMm,
|
||||
heightMm: imgHeightMm,
|
||||
widthPx: img.width,
|
||||
heightPx: img.height
|
||||
}
|
||||
};
|
||||
} finally {
|
||||
URL.revokeObjectURL(img.src);
|
||||
}
|
||||
}
|
||||
21
src/pages/tools/pdf/convert-to-pdf/types.ts
Normal file
21
src/pages/tools/pdf/convert-to-pdf/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type Orientation = 'portrait' | 'landscape';
|
||||
export type PageType = 'a4' | 'full';
|
||||
|
||||
export interface ImageSize {
|
||||
widthMm: number;
|
||||
heightMm: number;
|
||||
widthPx: number;
|
||||
heightPx: number;
|
||||
}
|
||||
|
||||
export interface FormValues {
|
||||
pageType: PageType;
|
||||
orientation: Orientation;
|
||||
scale: number;
|
||||
}
|
||||
|
||||
export const initialValues: FormValues = {
|
||||
pageType: 'full',
|
||||
orientation: 'portrait',
|
||||
scale: 100
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { tool as compressPdfTool } from './compress-pdf/meta';
|
||||
import { tool as protectPdfTool } from './protect-pdf/meta';
|
||||
import { meta as pdfToEpub } from './pdf-to-epub/meta';
|
||||
import { tool as pdfEditor } from './editor/meta';
|
||||
import { tool as convertToPdf } from './convert-to-pdf/meta';
|
||||
|
||||
export const pdfTools: DefinedTool[] = [
|
||||
pdfEditor,
|
||||
@@ -16,5 +17,6 @@ export const pdfTools: DefinedTool[] = [
|
||||
protectPdfTool,
|
||||
mergePdf,
|
||||
pdfToEpub,
|
||||
pdfPdfToPng
|
||||
pdfPdfToPng,
|
||||
convertToPdf
|
||||
];
|
||||
|
||||
@@ -70,7 +70,9 @@ export async function splitPdf(
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const newFileName = pdfFile.name.replace('.pdf', '-extracted.pdf');
|
||||
return new File([newPdfBytes], newFileName, { type: 'application/pdf' });
|
||||
return new File([newPdfBytes as any], newFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +91,7 @@ export async function mergePdf(pdfFiles: File[]): Promise<File> {
|
||||
|
||||
const mergedPdfBytes = await mergedPdf.save();
|
||||
const mergedFileName = 'merged.pdf';
|
||||
return new File([mergedPdfBytes], mergedFileName, {
|
||||
return new File([mergedPdfBytes as any], mergedFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function convertPdfToPngImages(pdfFile: File): Promise<{
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
await page.render({ canvas, canvasContext: context, viewport }).promise;
|
||||
|
||||
const blob = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob((b) => b && resolve(b), 'image/png')
|
||||
|
||||
@@ -78,5 +78,7 @@ export async function rotatePdf(
|
||||
const modifiedPdfBytes = await pdfDoc.save();
|
||||
const newFileName = pdfFile.name.replace('.pdf', '-rotated.pdf');
|
||||
|
||||
return new File([modifiedPdfBytes], newFileName, { type: 'application/pdf' });
|
||||
return new File([modifiedPdfBytes as any], newFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,5 +70,7 @@ export async function splitPdf(
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
const newFileName = pdfFile.name.replace('.pdf', '-extracted.pdf');
|
||||
return new File([newPdfBytes], newFileName, { type: 'application/pdf' });
|
||||
return new File([newPdfBytes as any], newFileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { tool as stringCensor } from './censor/meta';
|
||||
import { tool as stringPasswordGenerator } from './password-generator/meta';
|
||||
import { tool as stringEncodeUrl } from './url-encode/meta';
|
||||
import { tool as StringDecodeUrl } from './url-decode/meta';
|
||||
import { tool as stringUnicode } from './unicode/meta';
|
||||
|
||||
export const stringTools = [
|
||||
stringSplit,
|
||||
@@ -45,5 +46,6 @@ export const stringTools = [
|
||||
stringPasswordGenerator,
|
||||
stringEncodeUrl,
|
||||
StringDecodeUrl,
|
||||
stringUnicode,
|
||||
stringHiddenCharacterDetector
|
||||
];
|
||||
|
||||
123
src/pages/tools/string/unicode/index.tsx
Normal file
123
src/pages/tools/string/unicode/index.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { unicode } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
import SimpleRadio from '@components/options/SimpleRadio';
|
||||
import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
mode: 'encode',
|
||||
uppercase: false
|
||||
};
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Encode to Unicode Escape',
|
||||
description: 'Encode plain text to Unicode escape sequences.',
|
||||
sampleText: 'Hello, World!',
|
||||
sampleResult:
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021',
|
||||
sampleOptions: {
|
||||
mode: 'encode',
|
||||
uppercase: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Encode to Unicode Escape (Uppercase)',
|
||||
description: 'Encode plain text to uppercase Unicode escape sequences.',
|
||||
sampleText: 'Hello, World!',
|
||||
sampleResult:
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021'.toUpperCase(),
|
||||
sampleOptions: {
|
||||
mode: 'encode',
|
||||
uppercase: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Decode Unicode Escape',
|
||||
description: 'Decode Unicode escape sequences back to plain text.',
|
||||
sampleText:
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021',
|
||||
sampleResult: 'Hello, World!',
|
||||
sampleOptions: {
|
||||
mode: 'decode',
|
||||
uppercase: false
|
||||
}
|
||||
}
|
||||
];
|
||||
export default function Unicode({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('string');
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const compute = (values: InitialValuesType, input: string) => {
|
||||
setResult(unicode(input, values));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: t('unicode.optionsTitle'),
|
||||
component: (
|
||||
<Box>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('mode', 'encode')}
|
||||
checked={values.mode === 'encode'}
|
||||
title={t('unicode.encode')}
|
||||
/>
|
||||
<SimpleRadio
|
||||
onClick={() => updateField('mode', 'decode')}
|
||||
checked={values.mode === 'decode'}
|
||||
title={t('unicode.decode')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('unicode.caseOptionsTitle'),
|
||||
component: (
|
||||
<Box>
|
||||
<CheckboxWithDesc
|
||||
checked={values.uppercase}
|
||||
onChange={(value) => updateField('uppercase', value)}
|
||||
title={t('unicode.uppercase')}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolTextInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={t('unicode.inputTitle')}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolTextResult value={result} title={t('unicode.resultTitle')} />
|
||||
}
|
||||
initialValues={initialValues}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{
|
||||
title: t('unicode.toolInfo.title'),
|
||||
description: t('unicode.toolInfo.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
14
src/pages/tools/string/unicode/meta.ts
Normal file
14
src/pages/tools/string/unicode/meta.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:unicode.title',
|
||||
description: 'string:unicode.description',
|
||||
shortDescription: 'string:unicode.shortDescription'
|
||||
},
|
||||
path: 'unicode',
|
||||
icon: 'mdi:unicode',
|
||||
keywords: ['unicode', 'encode', 'decode', 'escape', 'text'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
21
src/pages/tools/string/unicode/service.ts
Normal file
21
src/pages/tools/string/unicode/service.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { InitialValuesType } from './types';
|
||||
|
||||
export function unicode(input: string, options: InitialValuesType): string {
|
||||
if (!input) return '';
|
||||
if (options.mode === 'encode') {
|
||||
let result = '';
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
let hex = input.charCodeAt(i).toString(16);
|
||||
hex = ('0000' + hex).slice(-4);
|
||||
if (options.uppercase) {
|
||||
hex = hex.toUpperCase();
|
||||
}
|
||||
result += '\\u' + hex;
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return input.replace(/\\u([\dA-Fa-f]{4})/g, (match, grp) => {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
});
|
||||
}
|
||||
}
|
||||
4
src/pages/tools/string/unicode/types.ts
Normal file
4
src/pages/tools/string/unicode/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type InitialValuesType = {
|
||||
mode: 'encode' | 'decode';
|
||||
uppercase: boolean;
|
||||
};
|
||||
178
src/pages/tools/string/unicode/unicode.service.test.ts
Normal file
178
src/pages/tools/string/unicode/unicode.service.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { unicode } from './service';
|
||||
|
||||
describe('unicode', () => {
|
||||
it('should encode an English string to lowercase hex correctly', () => {
|
||||
const input = 'Hello, World!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode an English string to uppercase hex correctly', () => {
|
||||
const input = 'Hello, World!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\u0048\\u0065\\u006C\\u006C\\u006F\\u002C\\u0020\\u0057\\u006F\\u0072\\u006C\\u0064\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode an English lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u0048\\u0065\\u006c\\u006c\\u006f\\u002c\\u0020\\u0057\\u006f\\u0072\\u006c\\u0064\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
it('should decode an English uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u0048\\u0065\\u006C\\u006C\\u006F\\u002C\\u0020\\u0057\\u006F\\u0072\\u006C\\u0064\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
it('should encode a Korean string to lowercase hex correctly', () => {
|
||||
const input = '안녕하세요, 세계!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\uc548\\ub155\\ud558\\uc138\\uc694\\u002c\\u0020\\uc138\\uacc4\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode a Korean string to uppercase hex correctly', () => {
|
||||
const input = '안녕하세요, 세계!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\uC548\\uB155\\uD558\\uC138\\uC694\\u002C\\u0020\\uC138\\uACC4\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode a Korean lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\uc548\\ub155\\ud558\\uc138\\uc694\\u002c\\u0020\\uc138\\uacc4\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('안녕하세요, 세계!');
|
||||
});
|
||||
|
||||
it('should decode a Korean uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\uC548\\uB155\\uD558\\uC138\\uC694\\u002C\\u0020\\uC138\\uACC4\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('안녕하세요, 세계!');
|
||||
});
|
||||
|
||||
it('should encode a Japanese string to lowercase hex correctly', () => {
|
||||
const input = 'こんにちは、世界!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\u3053\\u3093\\u306b\\u3061\\u306f\\u3001\\u4e16\\u754c\\uff01'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode a Japanese string to uppercase hex correctly', () => {
|
||||
const input = 'こんにちは、世界!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\u3053\\u3093\\u306B\\u3061\\u306F\\u3001\\u4E16\\u754C\\uFF01'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode a Japanese lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u3053\\u3093\\u306b\\u3061\\u306f\\u3001\\u4e16\\u754c\\uff01';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('こんにちは、世界!');
|
||||
});
|
||||
|
||||
it('should decode a Japanese uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u3053\\u3093\\u306B\\u3061\\u306F\\u3001\\u4E16\\u754C\\uFF01';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('こんにちは、世界!');
|
||||
});
|
||||
|
||||
it('should encode a Chinese string to lowercase hex correctly', () => {
|
||||
const input = '你好,世界!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe('\\u4f60\\u597d\\uff0c\\u4e16\\u754c\\uff01');
|
||||
});
|
||||
|
||||
it('should encode a Chinese string to uppercase hex correctly', () => {
|
||||
const input = '你好,世界!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe('\\u4F60\\u597D\\uFF0C\\u4E16\\u754C\\uFF01');
|
||||
});
|
||||
|
||||
it('should decode a Chinese lowercase hex string correctly', () => {
|
||||
const input = '\\u4f60\\u597d\\uff0c\\u4e16\\u754c\\uff01';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('你好,世界!');
|
||||
});
|
||||
|
||||
it('should decode a Chinese uppercase hex string correctly', () => {
|
||||
const input = '\\u4F60\\u597D\\uFF0C\\u4E16\\u754C\\uFF01';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('你好,世界!');
|
||||
});
|
||||
|
||||
it('should encode a Russian string to lowercase hex correctly', () => {
|
||||
const input = 'Привет, мир!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442\\u002c\\u0020\\u043c\\u0438\\u0440\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode a Russian string to uppercase hex correctly', () => {
|
||||
const input = 'Привет, мир!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\u041F\\u0440\\u0438\\u0432\\u0435\\u0442\\u002C\\u0020\\u043C\\u0438\\u0440\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode a Russian lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442\\u002c\\u0020\\u043c\\u0438\\u0440\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('Привет, мир!');
|
||||
});
|
||||
|
||||
it('should decode a Russian uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u041F\\u0440\\u0438\\u0432\\u0435\\u0442\\u002C\\u0020\\u043C\\u0438\\u0440\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('Привет, мир!');
|
||||
});
|
||||
|
||||
it('should encode a Spanish string to lowercase hex correctly', () => {
|
||||
const input = '¡Hola, Mundo!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: false });
|
||||
expect(result).toBe(
|
||||
'\\u00a1\\u0048\\u006f\\u006c\\u0061\\u002c\\u0020\\u004d\\u0075\\u006e\\u0064\\u006f\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should encode a Spanish string to uppercase hex correctly', () => {
|
||||
const input = '¡Hola, Mundo!';
|
||||
const result = unicode(input, { mode: 'encode', uppercase: true });
|
||||
expect(result).toBe(
|
||||
'\\u00A1\\u0048\\u006F\\u006C\\u0061\\u002C\\u0020\\u004D\\u0075\\u006E\\u0064\\u006F\\u0021'
|
||||
);
|
||||
});
|
||||
|
||||
it('should decode a Spanish lowercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u00a1\\u0048\\u006f\\u006c\\u0061\\u002c\\u0020\\u004d\\u0075\\u006e\\u0064\\u006f\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('¡Hola, Mundo!');
|
||||
});
|
||||
|
||||
it('should decode a Spanish uppercase hex string correctly', () => {
|
||||
const input =
|
||||
'\\u00A1\\u0048\\u006F\\u006C\\u0061\\u002C\\u0020\\u004D\\u0075\\u006E\\u0064\\u006F\\u0021';
|
||||
const result = unicode(input, { mode: 'decode', uppercase: false });
|
||||
expect(result).toBe('¡Hola, Mundo!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
import { convertTimeToDecimal } from './service';
|
||||
|
||||
describe('convert-time-to-decimal', () => {
|
||||
it('should convert time to decimal with default decimal places', () => {
|
||||
const input = '31:23:59';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('31.399722');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with specified decimal places', () => {
|
||||
const input = '31:23:59';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '10' });
|
||||
expect(result).toBe('31.3997222222');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with supplied format of HH:MM:SS', () => {
|
||||
const input = '13:25:30';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('13.425000');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with supplied format of HH:MM', () => {
|
||||
const input = '13:25';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('13.416667');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with supplied format of HH:MM:', () => {
|
||||
const input = '13:25';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('13.416667');
|
||||
});
|
||||
|
||||
it('should convert time to decimal with supplied format of HH.MM.SS', () => {
|
||||
const input = '13.25.30';
|
||||
const result = convertTimeToDecimal(input, { decimalPlaces: '6' });
|
||||
expect(result).toBe('13.425000');
|
||||
});
|
||||
});
|
||||
72
src/pages/tools/time/convert-time-to-decimal/index.tsx
Normal file
72
src/pages/tools/time/convert-time-to-decimal/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import { CardExampleType } from '@components/examples/ToolExamples';
|
||||
import { convertTimeToDecimal } from './service';
|
||||
import { InitialValuesType } from './types';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
decimalPlaces: '6'
|
||||
};
|
||||
|
||||
const exampleCards: CardExampleType<InitialValuesType>[] = [
|
||||
{
|
||||
title: 'Convert time to decimal',
|
||||
description:
|
||||
'This example shows how to convert a formatted time (HH:MM:SS) to a decimal version.',
|
||||
sampleText: '31:23:59',
|
||||
sampleResult: `31.399722`,
|
||||
sampleOptions: {
|
||||
decimalPlaces: '6'
|
||||
}
|
||||
}
|
||||
];
|
||||
export default function ConvertTimeToDecimal({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const [input, setInput] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const compute = (values: InitialValuesType, input: string) => {
|
||||
setResult(convertTimeToDecimal(input, values));
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||
values,
|
||||
updateField
|
||||
}) => [
|
||||
{
|
||||
title: 'Decimal places',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
description={'How many decimal places should the result contain?'}
|
||||
value={values.decimalPlaces}
|
||||
onOwnChange={(val) => updateField('decimalPlaces', val)}
|
||||
type={'text'}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={<ToolTextInput value={input} onChange={setInput} />}
|
||||
resultComponent={<ToolTextResult value={result} />}
|
||||
initialValues={initialValues}
|
||||
exampleCards={exampleCards}
|
||||
getGroups={getGroups}
|
||||
setInput={setInput}
|
||||
compute={compute}
|
||||
toolInfo={{ title: `What is a ${title}?`, description: longDescription }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
15
src/pages/tools/time/convert-time-to-decimal/meta.ts
Normal file
15
src/pages/tools/time/convert-time-to-decimal/meta.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('time', {
|
||||
i18n: {
|
||||
name: 'time:convertTimeToDecimal.title',
|
||||
description: 'time:convertTimeToDecimal.description',
|
||||
shortDescription: 'time:convertTimeToDecimal.shortDescription',
|
||||
longDescription: 'time:convertTimeToDecimal.longDescription'
|
||||
},
|
||||
path: 'convert-time-to-decimal',
|
||||
icon: 'material-symbols-light:decimal-increase-rounded',
|
||||
keywords: ['convert', 'time', 'to', 'decimal'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
37
src/pages/tools/time/convert-time-to-decimal/service.ts
Normal file
37
src/pages/tools/time/convert-time-to-decimal/service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { InitialValuesType } from './types';
|
||||
import { humanTimeValidation } from 'utils/time';
|
||||
|
||||
export function convertTimeToDecimal(
|
||||
input: string,
|
||||
options: InitialValuesType
|
||||
): string {
|
||||
if (!input) return '';
|
||||
|
||||
const dp = parseInt(options.decimalPlaces, 10);
|
||||
if (isNaN(dp) || dp < 0) {
|
||||
return 'Invalid decimal places value.';
|
||||
}
|
||||
|
||||
// Multiple lines processing
|
||||
const lines = input.split('\n');
|
||||
if (!lines) return '';
|
||||
|
||||
const result: string[] = [];
|
||||
|
||||
lines.forEach((line) => {
|
||||
line = line.trim();
|
||||
if (!line) return;
|
||||
|
||||
const { isValid, hours, minutes, seconds } = humanTimeValidation(line);
|
||||
|
||||
if (!isValid) {
|
||||
result.push('Incorrect input format use `HH:MM:(SS)` or `HH.MM.(SS )`.');
|
||||
return;
|
||||
}
|
||||
|
||||
const decimalTime = hours + minutes / 60 + seconds / 3600;
|
||||
result.push(decimalTime.toFixed(dp).toString());
|
||||
});
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
3
src/pages/tools/time/convert-time-to-decimal/types.ts
Normal file
3
src/pages/tools/time/convert-time-to-decimal/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type InitialValuesType = {
|
||||
decimalPlaces: string;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { tool as timeConvertTimeToDecimal } from './convert-time-to-decimal/meta';
|
||||
import { tool as timeConvertUnixToDate } from './convert-unix-to-date/meta';
|
||||
import { tool as timeCrontabGuru } from './crontab-guru/meta';
|
||||
import { tool as timeBetweenDates } from './time-between-dates/meta';
|
||||
@@ -17,5 +18,6 @@ export const timeTools = [
|
||||
timeBetweenDates,
|
||||
timeCrontabGuru,
|
||||
checkLeapYear,
|
||||
timeConvertUnixToDate
|
||||
timeConvertUnixToDate,
|
||||
timeConvertTimeToDecimal
|
||||
];
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function ChangeSpeed({
|
||||
const data = await ffmpeg.readFile(outputName);
|
||||
|
||||
// Create new file from processed data
|
||||
const blob = new Blob([data], { type: 'video/mp4' });
|
||||
const blob = new Blob([data as any], { type: 'video/mp4' });
|
||||
const newFile = new File(
|
||||
[blob],
|
||||
file.name.replace('.mp4', `-${newSpeed}x.mp4`),
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function compressVideo(
|
||||
}
|
||||
const compressedData = await ffmpeg.readFile(outputName);
|
||||
return new File(
|
||||
[new Blob([compressedData], { type: 'video/mp4' })],
|
||||
[new Blob([compressedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_compressed_${options.width}p.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function cropVideo(
|
||||
|
||||
const croppedData = await ffmpeg.readFile(outputName);
|
||||
return await new File(
|
||||
[new Blob([croppedData], { type: 'video/mp4' })],
|
||||
[new Blob([croppedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_cropped.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function flipVideo(
|
||||
|
||||
const flippedData = await ffmpeg.readFile(outputName);
|
||||
return new File(
|
||||
[new Blob([flippedData], { type: 'video/mp4' })],
|
||||
[new Blob([flippedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_flipped.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function ChangeSpeed({ title }: ToolComponentProps) {
|
||||
const data = await ffmpeg.readFile('output.gif');
|
||||
|
||||
// Create a new file from the processed data
|
||||
const blob = new Blob([data], { type: 'image/gif' });
|
||||
const blob = new Blob([data as any], { type: 'image/gif' });
|
||||
const newFile = new File(
|
||||
[blob],
|
||||
file.name.replace('.gif', `-${newSpeed}x.gif`),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('video', {
|
||||
path: 'loop',
|
||||
icon: 'material-symbols:loop',
|
||||
icon: 'ic:outline-loop',
|
||||
|
||||
keywords: ['video', 'loop', 'repeat', 'continuous'],
|
||||
component: lazy(() => import('./index')),
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function loopVideo(
|
||||
|
||||
const loopedData = await ffmpeg.readFile(outputName);
|
||||
return await new File(
|
||||
[new Blob([loopedData], { type: 'video/mp4' })],
|
||||
[new Blob([loopedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_looped.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@@ -112,7 +112,7 @@ export async function mergeVideos(
|
||||
throw new Error('Output file is empty or corrupted');
|
||||
}
|
||||
|
||||
return new Blob([mergedData], { type: 'video/mp4' });
|
||||
return new Blob([mergedData as any], { type: 'video/mp4' });
|
||||
} catch (error) {
|
||||
console.error('Error merging videos:', error);
|
||||
throw error instanceof Error
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function rotateVideo(
|
||||
|
||||
const rotatedData = await ffmpeg.readFile(outputName);
|
||||
return new File(
|
||||
[new Blob([rotatedData], { type: 'video/mp4' })],
|
||||
[new Blob([rotatedData as any], { type: 'video/mp4' })],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_rotated.mp4`,
|
||||
{ type: 'video/mp4' }
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function TrimVideo({ title }: ToolComponentProps) {
|
||||
]);
|
||||
// Retrieve the processed file
|
||||
const trimmedData = await ffmpeg.readFile(outputName);
|
||||
const trimmedBlob = new Blob([trimmedData], { type: 'video/mp4' });
|
||||
const trimmedBlob = new Blob([trimmedData as any], { type: 'video/mp4' });
|
||||
const trimmedFile = new File(
|
||||
[trimmedBlob],
|
||||
`${input.name.replace(/\.[^/.]+$/, '')}_trimmed.mp4`,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { GetGroupsType } from '@components/options/ToolOptions';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { updateNumberField } from '@utils/string';
|
||||
import { InitialValuesType } from './types';
|
||||
import ToolVideoInput from '@components/input/ToolVideoInput';
|
||||
import ToolFileResult from '@components/result/ToolFileResult';
|
||||
@@ -13,9 +16,19 @@ import { fetchFile } from '@ffmpeg/util';
|
||||
const initialValues: InitialValuesType = {
|
||||
quality: 'mid',
|
||||
fps: '10',
|
||||
scale: '320:-1:flags=bicubic'
|
||||
scale: '320:-1:flags=bicubic',
|
||||
start: 0,
|
||||
end: 100
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
start: Yup.number().min(0, 'Start time must be positive'),
|
||||
end: Yup.number().min(
|
||||
Yup.ref('start'),
|
||||
'End time must be greater than start time'
|
||||
)
|
||||
});
|
||||
|
||||
export default function VideoToGif({
|
||||
title,
|
||||
longDescription
|
||||
@@ -26,14 +39,16 @@ export default function VideoToGif({
|
||||
|
||||
const compute = (values: InitialValuesType, input: File | null) => {
|
||||
if (!input) return;
|
||||
const { fps, scale } = values;
|
||||
const { fps, scale, start, end } = values;
|
||||
let ffmpeg: FFmpeg | null = null;
|
||||
let ffmpegLoaded = false;
|
||||
|
||||
const convertVideoToGif = async (
|
||||
file: File,
|
||||
fps: string,
|
||||
scale: string
|
||||
scale: string,
|
||||
start: number,
|
||||
end: number
|
||||
): Promise<void> => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -58,6 +73,10 @@ export default function VideoToGif({
|
||||
await ffmpeg.exec([
|
||||
'-i',
|
||||
fileName,
|
||||
'-ss',
|
||||
start.toString(),
|
||||
'-to',
|
||||
end.toString(),
|
||||
'-vf',
|
||||
`fps=${fps},scale=${scale},palettegen`,
|
||||
'palette.png'
|
||||
@@ -68,6 +87,10 @@ export default function VideoToGif({
|
||||
fileName,
|
||||
'-i',
|
||||
'palette.png',
|
||||
'-ss',
|
||||
start.toString(),
|
||||
'-to',
|
||||
end.toString(),
|
||||
'-filter_complex',
|
||||
`fps=${fps},scale=${scale}[x];[x][1:v]paletteuse`,
|
||||
outputName
|
||||
@@ -75,7 +98,7 @@ export default function VideoToGif({
|
||||
|
||||
const data = await ffmpeg.readFile(outputName);
|
||||
|
||||
const blob = new Blob([data], { type: 'image/gif' });
|
||||
const blob = new Blob([data as any], { type: 'image/gif' });
|
||||
const convertedFile = new File([blob], outputName, {
|
||||
type: 'image/gif'
|
||||
});
|
||||
@@ -92,7 +115,7 @@ export default function VideoToGif({
|
||||
}
|
||||
};
|
||||
|
||||
convertVideoToGif(input, fps, scale);
|
||||
convertVideoToGif(input, fps, scale, start, end);
|
||||
};
|
||||
|
||||
const getGroups: GetGroupsType<InitialValuesType> | null = ({
|
||||
@@ -141,6 +164,28 @@ export default function VideoToGif({
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: 'Timestamps',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
onOwnChange={(value) =>
|
||||
updateNumberField(value, 'start', updateField)
|
||||
}
|
||||
value={values.start}
|
||||
label="Start Time"
|
||||
sx={{ mb: 2, backgroundColor: 'background.paper' }}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
onOwnChange={(value) =>
|
||||
updateNumberField(value, 'end', updateField)
|
||||
}
|
||||
value={values.end}
|
||||
label="End Time"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
@@ -148,9 +193,22 @@ export default function VideoToGif({
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
inputComponent={
|
||||
<ToolVideoInput value={input} onChange={setInput} title="Input Video" />
|
||||
}
|
||||
renderCustomInput={({ start, end }, setFieldValue) => {
|
||||
return (
|
||||
<ToolVideoInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
title={'Input Video'}
|
||||
showTrimControls={true}
|
||||
onTrimChange={(start, end) => {
|
||||
setFieldValue('start', start);
|
||||
setFieldValue('end', end);
|
||||
}}
|
||||
trimStart={start}
|
||||
trimEnd={end}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
resultComponent={
|
||||
loading ? (
|
||||
<ToolFileResult
|
||||
|
||||
@@ -2,4 +2,6 @@ export type InitialValuesType = {
|
||||
quality: 'mid' | 'high' | 'low' | 'ultra';
|
||||
fps: string;
|
||||
scale: string;
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
|
||||
58
src/utils/time.ts
Normal file
58
src/utils/time.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
type TimeValidationResult = {
|
||||
isValid: boolean;
|
||||
hours: number;
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates human-readable time format (HH:MM or HH:MM:SS)
|
||||
* Supports either ':' or '.' as a separator, but not both
|
||||
* @param {string} input - string time format
|
||||
* * @returns {{
|
||||
* isValid: boolean, // true if the input is a valid time
|
||||
* hours: number, // parsed hours (0 or greater)
|
||||
* minutes: number, // parsed minutes (0-59)
|
||||
* seconds: number // parsed seconds (0-59, 0 if not provided)
|
||||
* }}
|
||||
*/
|
||||
export function humanTimeValidation(input: string): TimeValidationResult {
|
||||
const result = { isValid: false, hours: 0, minutes: 0, seconds: 0 };
|
||||
|
||||
if (!input) return result;
|
||||
|
||||
input = input.trim();
|
||||
|
||||
// Operator use validation
|
||||
// use of one between these two operators '.' or ':'
|
||||
|
||||
const hasColon = input.includes(':');
|
||||
const hasDot = input.includes('.');
|
||||
|
||||
if (hasColon && hasDot) return result;
|
||||
|
||||
if (!hasColon && !hasDot) return result;
|
||||
|
||||
const separator = hasColon ? ':' : '.';
|
||||
|
||||
// Time parts validation
|
||||
|
||||
const parts = input.split(separator);
|
||||
|
||||
if (parts.length < 2 || parts.length > 3) return result;
|
||||
|
||||
const [h, m, s = '0'] = parts;
|
||||
|
||||
// every character should be a digit
|
||||
if (![h, m, s].every((x) => /^\d+$/.test(x))) return result;
|
||||
|
||||
const hours = parseInt(h);
|
||||
const minutes = parseInt(m);
|
||||
const seconds = parseInt(s);
|
||||
|
||||
if (minutes < 0 || minutes > 59) return result;
|
||||
if (seconds < 0 || seconds > 59) return result;
|
||||
if (hours < 0) return result;
|
||||
|
||||
return { isValid: true, hours, minutes, seconds };
|
||||
}
|
||||
Reference in New Issue
Block a user