From 4d45c694b78ddbdf8b9647495f47115be7008394 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Sun, 30 Mar 2025 15:48:48 +0000 Subject: [PATCH 1/8] fix: typos --- src/pages/tools/csv/csv-to-tsv/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/tools/csv/csv-to-tsv/index.tsx b/src/pages/tools/csv/csv-to-tsv/index.tsx index 995cf87..49898df 100644 --- a/src/pages/tools/csv/csv-to-tsv/index.tsx +++ b/src/pages/tools/csv/csv-to-tsv/index.tsx @@ -22,7 +22,7 @@ const exampleCards: CardExampleType[] = [ { title: 'Convert Game Data from the CSV Format to the TSV Format', description: - 'This tool transforms Comma Separated Values (CSV) data to Tab Separated Values (TSV) data. Both CSV and TSV are popular file formats for storing tabular data but they use different delimiters to separate values – CSV uses commas (","), while TSV uses tabs ("\t"). If we compare CSV files to TSV files, then CSV files are much harder to parse than TSV files because the values themselves may contain commas, so it is not always obvious where one field starts and ends without complicated parsing rules. TSV, on the other hand, uses just a tab symbol, which does not usually appear in data, so separating fields in TSV is as simple as splitting the input by the tab character. To convert CSV to TSV, simply input the CSV data in the input of this tool. In rare cases when a CSV file has a delimiter other than a comma, you can specify the current delimiter in the options of the tool. You can also specify the current quote character and the comment start character. Additionally, empty CSV lines can be skipped by activating the "Ignore Lines with No Data" option. If this option is off, then empty lines in the CSV are converted to empty TSV lines. The "Preserve Headers" option allows you to choose whether to process column headers of a CSV file. If the option is selected, then the resulting TSV file will include the first row of the input CSV file, which contains the column names. Alternatively, if the headers option is not selected, the first row will be skipped during the data conversion process. For the reverse conversion from TSV to CSV, you can use our Convert TSV to CSV tool. Csv-abulous!', + 'In this example, we transform a Comma Separated Values (CSV) file containing a leaderboard of gaming data into a Tab Separated Values (TSV) file. The input data shows the players\' names, scores, times, and goals. We preserve the CSV column headers by enabling the "Preserve Headers" option and convert all data rows into TSV format. The resulting data is easier to work with as it\'s organized in neat columns', sampleText: `player_name,score,time,goals ToniJackson,2500,30:00,15 HenryDalton,1800,25:00,12 @@ -54,7 +54,7 @@ Vampire;Mythology;Castles;Immortality Phoenix;Mythology;Desert;Rebirth from ashes #Dragon;Mythology;Mountains;Fire breathing -#Werewolf;Mythology;Forests;Shapeshifting`, +#Werewolf;Mythology;Forests;Shape shifting`, sampleResult: `Unicorn Mythology Forest Magic horn Mermaid Mythology Ocean Hypnotic singing Vampire Mythology Castles Immortality @@ -68,7 +68,7 @@ Phoenix Mythology Desert Rebirth from ashes`, } }, { - title: 'Convet Fitness Tracker Data from CSV to TSV', + title: 'Convert Fitness Tracker Data from CSV to TSV', description: 'In this example, we swap rows and columns in CSV data about team sports, the equipment used, and the number of players. The input has 5 rows and 3 columns and once rows and columns have been swapped, the output has 3 rows and 5 columns. Also notice that in the last data record, for the "Baseball" game, the number of players is missing. To create a fully-filled CSV, we use a custom message "NA", specified in the options, and fill the missing CSV field with this value.', sampleText: `day,steps,distance,calories From 8b4384b499b374afacee42a96ac18e540eea4013 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Sun, 30 Mar 2025 17:18:06 +0000 Subject: [PATCH 2/8] feat: compress video --- src/pages/tools/video/compress/index.tsx | 183 ++++++++++++++++++++++ src/pages/tools/video/compress/meta.ts | 20 +++ src/pages/tools/video/compress/service.ts | 61 ++++++++ src/pages/tools/video/index.ts | 3 +- 4 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 src/pages/tools/video/compress/index.tsx create mode 100644 src/pages/tools/video/compress/meta.ts create mode 100644 src/pages/tools/video/compress/service.ts diff --git a/src/pages/tools/video/compress/index.tsx b/src/pages/tools/video/compress/index.tsx new file mode 100644 index 0000000..7fa71e3 --- /dev/null +++ b/src/pages/tools/video/compress/index.tsx @@ -0,0 +1,183 @@ +import { Box } from '@mui/material'; +import React, { useCallback, useState } from 'react'; +import * as Yup from 'yup'; +import ToolFileResult from '@components/result/ToolFileResult'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import { GetGroupsType } from '@components/options/ToolOptions'; +import { debounce } from 'lodash'; +import ToolVideoInput from '@components/input/ToolVideoInput'; +import { compressVideo, VideoResolution } from './service'; +import SimpleRadio from '@components/options/SimpleRadio'; +import Slider from 'rc-slider'; +import 'rc-slider/assets/index.css'; + +export const initialValues = { + width: 480 as VideoResolution, + crf: 23, + preset: 'medium' +}; + +export const validationSchema = Yup.object({ + width: Yup.number() + .oneOf( + [240, 360, 480, 720, 1080], + 'Width must be one of the standard resolutions' + ) + .required('Width is required'), + crf: Yup.number() + .min(0, 'CRF must be at least 0') + .max(51, 'CRF must be at most 51') + .required('CRF is required'), + preset: Yup.string() + .oneOf( + [ + 'ultrafast', + 'superfast', + 'veryfast', + 'faster', + 'fast', + 'medium', + 'slow', + 'slower', + 'veryslow' + ], + 'Preset must be a valid ffmpeg preset' + ) + .required('Preset is required') +}); + +const resolutionOptions: { value: VideoResolution; label: string }[] = [ + { value: 240, label: '240p' }, + { value: 360, label: '360p' }, + { value: 480, label: '480p' }, + { value: 720, label: '720p' }, + { value: 1080, label: '1080p' } +]; + +const presetOptions = [ + { value: 'ultrafast', label: 'Ultrafast (Lowest Quality, Smallest Size)' }, + { value: 'superfast', label: 'Superfast' }, + { value: 'veryfast', label: 'Very Fast' }, + { value: 'faster', label: 'Faster' }, + { value: 'fast', label: 'Fast' }, + { value: 'medium', label: 'Medium (Balanced)' }, + { value: 'slow', label: 'Slow' }, + { value: 'slower', label: 'Slower' }, + { value: 'veryslow', label: 'Very Slow (Highest Quality, Largest Size)' } +]; + +export default function CompressVideo({ title }: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const compute = async ( + optionsValues: typeof initialValues, + input: File | null + ) => { + if (!input) return; + setLoading(true); + + try { + const compressedFile = await compressVideo(input, { + width: optionsValues.width, + crf: optionsValues.crf, + preset: optionsValues.preset + }); + setResult(compressedFile); + } catch (error) { + console.error('Error compressing video:', error); + } finally { + setLoading(false); + } + }; + + const debouncedCompute = useCallback(debounce(compute, 1000), []); + + const getGroups: GetGroupsType = ({ + values, + updateField + }) => [ + { + title: 'Resolution', + component: ( + + {resolutionOptions.map((option) => ( + { + updateField('width', option.value); + }} + /> + ))} + + ) + }, + { + title: 'Quality (CRF)', + component: ( + + { + updateField('crf', typeof value === 'number' ? value : value[0]); + }} + marks={{ + 0: 'Lossless', + 23: 'Default', + 51: 'Worst' + }} + /> + + ) + } + // { + // title: 'Encoding Preset', + // component: ( + // updateField('preset', value)} + // options={presetOptions} + // description={ + // 'Determines the compression speed. Slower presets provide better compression (quality per filesize) but take more time.' + // } + // /> + // ) + // } + ]; + + return ( + + } + resultComponent={ + + } + initialValues={initialValues} + getGroups={getGroups} + compute={debouncedCompute} + setInput={setInput} + validationSchema={validationSchema} + /> + ); +} diff --git a/src/pages/tools/video/compress/meta.ts b/src/pages/tools/video/compress/meta.ts new file mode 100644 index 0000000..ab84e03 --- /dev/null +++ b/src/pages/tools/video/compress/meta.ts @@ -0,0 +1,20 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('video', { + name: 'Compress Video', + path: 'compress', + icon: 'mdi:video-box', + description: + 'Compress videos by scaling them to different resolutions like 240p, 480p, 720p, etc. This tool helps reduce file size while maintaining acceptable quality. Supports common video formats like MP4, WebM, and OGG.', + shortDescription: 'Compress videos by scaling to different resolutions', + keywords: [ + 'compress', + 'video', + 'resize', + 'scale', + 'resolution', + 'reduce size' + ], + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/video/compress/service.ts b/src/pages/tools/video/compress/service.ts new file mode 100644 index 0000000..2850dbd --- /dev/null +++ b/src/pages/tools/video/compress/service.ts @@ -0,0 +1,61 @@ +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { fetchFile } from '@ffmpeg/util'; + +const ffmpeg = new FFmpeg(); + +export type VideoResolution = 240 | 360 | 480 | 720 | 1080; + +export interface CompressVideoOptions { + width: VideoResolution; + crf: number; // Constant Rate Factor (quality): lower = better quality, higher = smaller file + preset: string; // Encoding speed preset +} + +export async function compressVideo( + input: File, + options: CompressVideoOptions +): Promise { + console.log('Compressing video...', options); + if (!ffmpeg.loaded) { + await ffmpeg.load({ + wasmURL: + 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm' + }); + } + + const inputName = 'input.mp4'; + const outputName = 'output.mp4'; + + await ffmpeg.writeFile(inputName, await fetchFile(input)); + + // Calculate height as -1 to maintain aspect ratio + const scaleFilter = `scale=${options.width}:-2`; + + const args = [ + '-i', + inputName, + '-vf', + scaleFilter, + '-c:v', + 'libx264', + '-crf', + options.crf.toString(), + '-preset', + options.preset, + '-c:a', + 'aac', // Copy audio stream + outputName + ]; + + try { + await ffmpeg.exec(args); + } catch (error) { + console.error('FFmpeg execution failed:', error); + } + const compressedData = await ffmpeg.readFile(outputName); + return new File( + [new Blob([compressedData], { type: 'video/mp4' })], + `${input.name.replace(/\.[^/.]+$/, '')}_compressed_${options.width}p.mp4`, + { type: 'video/mp4' } + ); +} diff --git a/src/pages/tools/video/index.ts b/src/pages/tools/video/index.ts index 9a9dd35..07405b1 100644 --- a/src/pages/tools/video/index.ts +++ b/src/pages/tools/video/index.ts @@ -2,5 +2,6 @@ import { rotate } from '../string/rotate/service'; import { gifTools } from './gif'; import { tool as trimVideo } from './trim/meta'; import { tool as rotateVideo } from './rotate/meta'; +import { tool as compressVideo } from './compress/meta'; -export const videoTools = [...gifTools, trimVideo, rotateVideo]; +export const videoTools = [...gifTools, trimVideo, rotateVideo, compressVideo]; From e53642db315d3b302593402cf7ed977f61b7cbf6 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Sun, 30 Mar 2025 17:19:22 +0000 Subject: [PATCH 3/8] chore: compress video icon --- src/pages/tools/video/compress/meta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tools/video/compress/meta.ts b/src/pages/tools/video/compress/meta.ts index ab84e03..a5075bd 100644 --- a/src/pages/tools/video/compress/meta.ts +++ b/src/pages/tools/video/compress/meta.ts @@ -4,7 +4,7 @@ import { lazy } from 'react'; export const tool = defineTool('video', { name: 'Compress Video', path: 'compress', - icon: 'mdi:video-box', + icon: 'icon-park-outline:compression', description: 'Compress videos by scaling them to different resolutions like 240p, 480p, 720p, etc. This tool helps reduce file size while maintaining acceptable quality. Supports common video formats like MP4, WebM, and OGG.', shortDescription: 'Compress videos by scaling to different resolutions', From d676383d228add48472d447241a4e9a091acfa50 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Mon, 31 Mar 2025 01:27:44 +0000 Subject: [PATCH 4/8] feat: dark mode --- .idea/workspace.xml | 213 ++++++---- @types/theme.d.ts | 9 + public/assets/background-dark.png | Bin 0 -> 143663 bytes src/components/App.tsx | 22 +- src/components/Hero.tsx | 13 +- src/components/Navbar/index.tsx | 16 +- src/components/ToolHeader.tsx | 3 +- src/components/ToolLayout.tsx | 2 +- src/components/allTools/ToolCard.tsx | 20 +- src/components/examples/ExampleCard.tsx | 6 +- src/components/input/BaseFileInput.tsx | 13 +- src/components/input/ToolFileInput.tsx | 377 ------------------ src/components/input/ToolTextInput.tsx | 2 +- src/components/options/ColorSelector.tsx | 2 +- src/components/options/TextFieldWithDesc.tsx | 2 +- src/components/options/ToolOptions.tsx | 2 +- src/components/result/ToolFileResult.tsx | 8 +- src/components/result/ToolTextResult.tsx | 2 +- src/config/muiConfig.ts | 45 ++- src/pages/home/Categories.tsx | 13 +- src/pages/home/index.tsx | 15 +- src/pages/tools-by-category/index.tsx | 17 +- src/pages/tools/pdf/split-pdf/index.tsx | 4 +- src/pages/tools/video/compress/service.ts | 1 - .../tools/video/gif/change-speed/index.tsx | 5 +- tsconfig.json | 36 +- 26 files changed, 297 insertions(+), 551 deletions(-) create mode 100644 @types/theme.d.ts create mode 100644 public/assets/background-dark.png delete mode 100644 src/components/input/ToolFileInput.tsx diff --git a/.idea/workspace.xml b/.idea/workspace.xml index dca2541..27386d7 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,11 +4,33 @@