From de19febda7cd32d1079fedb746a6e305f44ec848 Mon Sep 17 00:00:00 2001 From: AshAnand34 Date: Mon, 7 Jul 2025 22:44:26 -0700 Subject: [PATCH 01/34] feat: add video merging tool with multiple video input component --- .../input/ToolMultipleVideoInput.tsx | 92 +++++++++++++++++++ src/pages/tools/video/index.ts | 4 +- src/pages/tools/video/merge-video/index.tsx | 64 +++++++++++++ .../merge-video/merge-video.service.test.ts | 20 ++++ src/pages/tools/video/merge-video/meta.ts | 14 +++ src/pages/tools/video/merge-video/service.ts | 53 +++++++++++ src/pages/tools/video/merge-video/types.ts | 9 ++ 7 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/components/input/ToolMultipleVideoInput.tsx create mode 100644 src/pages/tools/video/merge-video/index.tsx create mode 100644 src/pages/tools/video/merge-video/merge-video.service.test.ts create mode 100644 src/pages/tools/video/merge-video/meta.ts create mode 100644 src/pages/tools/video/merge-video/service.ts create mode 100644 src/pages/tools/video/merge-video/types.ts diff --git a/src/components/input/ToolMultipleVideoInput.tsx b/src/components/input/ToolMultipleVideoInput.tsx new file mode 100644 index 0000000..e3eede2 --- /dev/null +++ b/src/components/input/ToolMultipleVideoInput.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { Box, Typography, IconButton } from '@mui/material'; +import BaseFileInput from './BaseFileInput'; +import { BaseFileInputProps } from './file-input-utils'; +import DeleteIcon from '@mui/icons-material/Delete'; + +interface ToolMultipleVideoInputProps + extends Omit { + value: File[] | null; + onChange: (files: File[]) => void; + accept?: string[]; + title?: string; +} + +export default function ToolMultipleVideoInput({ + value, + onChange, + accept = ['video/*', '.mkv'], + title, + ...props +}: ToolMultipleVideoInputProps) { + // For preview, use the first file if available + const preview = + value && value.length > 0 ? URL.createObjectURL(value[0]) : undefined; + + // Handler for file selection + const handleFileChange = (file: File | null) => { + if (file) { + // Add to existing files, avoiding duplicates by name + const files = value ? [...value] : []; + if (!files.some((f) => f.name === file.name && f.size === file.size)) { + onChange([...files, file]); + } + } + }; + + // Handler for removing a file + const handleRemove = (idx: number) => { + if (!value) return; + const newFiles = value.slice(); + newFiles.splice(idx, 1); + onChange(newFiles); + }; + + return ( + + {() => ( + + {preview && ( + + )} + + ); +} diff --git a/src/pages/tools/video/index.ts b/src/pages/tools/video/index.ts index 7cbf9f9..be61367 100644 --- a/src/pages/tools/video/index.ts +++ b/src/pages/tools/video/index.ts @@ -1,3 +1,4 @@ +import { tool as videoMergeVideo } from './merge-video/meta'; import { tool as videoToGif } from './video-to-gif/meta'; import { tool as changeSpeed } from './change-speed/meta'; import { tool as flipVideo } from './flip/meta'; @@ -17,5 +18,6 @@ export const videoTools = [ flipVideo, cropVideo, changeSpeed, - videoToGif + videoToGif, + videoMergeVideo ]; diff --git a/src/pages/tools/video/merge-video/index.tsx b/src/pages/tools/video/merge-video/index.tsx new file mode 100644 index 0000000..11f170b --- /dev/null +++ b/src/pages/tools/video/merge-video/index.tsx @@ -0,0 +1,64 @@ +import { Box } from '@mui/material'; +import React, { useState } from 'react'; +import ToolContent from '@components/ToolContent'; +import { ToolComponentProps } from '@tools/defineTool'; +import ToolFileResult from '@components/result/ToolFileResult'; +import ToolMultipleVideoInput from '@components/input/ToolMultipleVideoInput'; +import { main } from './service'; +import { InitialValuesType } from './types'; + +const initialValues: InitialValuesType = {}; + +export default function MergeVideo({ + title, + longDescription +}: ToolComponentProps) { + const [input, setInput] = useState(null); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(false); + + const compute = async (_values: InitialValuesType, input: File[] | null) => { + if (!input || input.length < 2) return; + setLoading(true); + try { + const mergedBlob = await main(input, initialValues); + const mergedFile = new File([mergedBlob], 'merged-video.mp4', { + type: 'video/mp4' + }); + setResult(mergedFile); + } catch (err) { + setResult(null); + } finally { + setLoading(false); + } + }; + + const getGroups = () => []; + + return ( + + } + resultComponent={ + + } + initialValues={initialValues} + getGroups={getGroups} + setInput={setInput} + compute={compute} + toolInfo={{ title: `What is a ${title}?`, description: longDescription }} + /> + ); +} diff --git a/src/pages/tools/video/merge-video/merge-video.service.test.ts b/src/pages/tools/video/merge-video/merge-video.service.test.ts new file mode 100644 index 0000000..6efe27d --- /dev/null +++ b/src/pages/tools/video/merge-video/merge-video.service.test.ts @@ -0,0 +1,20 @@ +import { expect, describe, it } from 'vitest'; +import { main } from './service'; + +function createMockFile(name: string, type = 'video/mp4') { + return new File([new Uint8Array([0, 1, 2])], name, { type }); +} + +describe('merge-video', () => { + it('throws if less than two files are provided', async () => { + await expect(main([], {})).rejects.toThrow(); + await expect(main([createMockFile('a.mp4')], {})).rejects.toThrow(); + }); + + it('merges two video files (mocked)', async () => { + // This will throw until ffmpeg logic is implemented + await expect( + main([createMockFile('a.mp4'), createMockFile('b.mp4')], {}) + ).rejects.toThrow('Video merging not yet implemented.'); + }); +}); diff --git a/src/pages/tools/video/merge-video/meta.ts b/src/pages/tools/video/merge-video/meta.ts new file mode 100644 index 0000000..7f459c5 --- /dev/null +++ b/src/pages/tools/video/merge-video/meta.ts @@ -0,0 +1,14 @@ +import { defineTool } from '@tools/defineTool'; +import { lazy } from 'react'; + +export const tool = defineTool('video', { + name: 'Merge Videos', + path: 'merge-video', + icon: 'merge_type', // Material icon for merging + description: 'Combine multiple video files into one continuous video.', + shortDescription: 'Append and merge videos easily.', + keywords: ['merge', 'video', 'append', 'combine'], + longDescription: + 'This tool allows you to merge or append multiple video files into a single continuous video. Simply upload your video files, arrange them in the desired order, and merge them into one file for easy sharing or editing.', + component: lazy(() => import('./index')) +}); diff --git a/src/pages/tools/video/merge-video/service.ts b/src/pages/tools/video/merge-video/service.ts new file mode 100644 index 0000000..71569a5 --- /dev/null +++ b/src/pages/tools/video/merge-video/service.ts @@ -0,0 +1,53 @@ +import { InitialValuesType, MergeVideoInput, MergeVideoOutput } from './types'; +import { FFmpeg } from '@ffmpeg/ffmpeg'; +import { fetchFile } from '@ffmpeg/util'; + +const ffmpeg = new FFmpeg(); + +// This function will use ffmpeg.wasm to merge multiple video files in the browser. +// Returns a Promise that resolves to a Blob of the merged video. +export async function main( + input: MergeVideoInput, + options: InitialValuesType +): Promise { + if (!Array.isArray(input) || input.length < 2) { + throw new Error('Please provide at least two video files to merge.'); + } + + if (!ffmpeg.loaded) { + await ffmpeg.load({ + wasmURL: + 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm' + }); + } + + // Write all input files to ffmpeg FS + const fileNames = input.map((file, idx) => `input${idx}.mp4`); + for (let i = 0; i < input.length; i++) { + await ffmpeg.writeFile(fileNames[i], await fetchFile(input[i])); + } + + // Create concat list file + const concatList = fileNames.map((name) => `file '${name}'`).join('\n'); + await ffmpeg.writeFile( + 'concat_list.txt', + new TextEncoder().encode(concatList) + ); + + // Run ffmpeg concat demuxer + const outputName = 'output.mp4'; + await ffmpeg.exec([ + '-f', + 'concat', + '-safe', + '0', + '-i', + 'concat_list.txt', + '-c', + 'copy', + outputName + ]); + + const mergedData = await ffmpeg.readFile(outputName); + return new Blob([mergedData], { type: 'video/mp4' }); +} diff --git a/src/pages/tools/video/merge-video/types.ts b/src/pages/tools/video/merge-video/types.ts new file mode 100644 index 0000000..7cb6a52 --- /dev/null +++ b/src/pages/tools/video/merge-video/types.ts @@ -0,0 +1,9 @@ +export type InitialValuesType = { + // Add any future options here (e.g., output format, resolution) +}; + +// Type for the main function input +export type MergeVideoInput = File[]; + +// Type for the main function output +export type MergeVideoOutput = Blob; From 2c7fc0b2d0a6007677176572a56b0884b7e477b7 Mon Sep 17 00:00:00 2001 From: AshAnand34 Date: Mon, 7 Jul 2025 23:04:20 -0700 Subject: [PATCH 02/34] Added icon for video merging --- src/pages/tools/video/merge-video/meta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tools/video/merge-video/meta.ts b/src/pages/tools/video/merge-video/meta.ts index 7f459c5..97a038d 100644 --- a/src/pages/tools/video/merge-video/meta.ts +++ b/src/pages/tools/video/merge-video/meta.ts @@ -4,7 +4,7 @@ import { lazy } from 'react'; export const tool = defineTool('video', { name: 'Merge Videos', path: 'merge-video', - icon: 'merge_type', // Material icon for merging + icon: 'carbon:video-add', description: 'Combine multiple video files into one continuous video.', shortDescription: 'Append and merge videos easily.', keywords: ['merge', 'video', 'append', 'combine'], From f64e91472fe173cc561285bc0b55317fbd6bc0c3 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 8 Jul 2025 22:17:51 +0100 Subject: [PATCH 03/34] chore: change icon --- src/pages/tools/video/merge-video/meta.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tools/video/merge-video/meta.ts b/src/pages/tools/video/merge-video/meta.ts index 97a038d..bbd90ec 100644 --- a/src/pages/tools/video/merge-video/meta.ts +++ b/src/pages/tools/video/merge-video/meta.ts @@ -4,7 +4,7 @@ import { lazy } from 'react'; export const tool = defineTool('video', { name: 'Merge Videos', path: 'merge-video', - icon: 'carbon:video-add', + icon: 'fluent:merge-20-regular', description: 'Combine multiple video files into one continuous video.', shortDescription: 'Append and merge videos easily.', keywords: ['merge', 'video', 'append', 'combine'], From 49b0ecb318c385e931b6c10639db735aaebde278 Mon Sep 17 00:00:00 2001 From: AshAnand34 Date: Fri, 11 Jul 2025 15:13:40 -0700 Subject: [PATCH 04/34] Fix rendering issue with ToolMultipleVideoInput as well as merge functionality. --- .../input/ToolMultipleVideoInput.tsx | 217 ++++++++++++------ src/pages/tools/video/merge-video/index.tsx | 39 +++- .../merge-video/merge-video.service.test.ts | 48 +++- src/pages/tools/video/merge-video/service.ts | 90 +++++--- 4 files changed, 276 insertions(+), 118 deletions(-) diff --git a/src/components/input/ToolMultipleVideoInput.tsx b/src/components/input/ToolMultipleVideoInput.tsx index e3eede2..9f42b8a 100644 --- a/src/components/input/ToolMultipleVideoInput.tsx +++ b/src/components/input/ToolMultipleVideoInput.tsx @@ -1,92 +1,177 @@ -import React from 'react'; -import { Box, Typography, IconButton } from '@mui/material'; -import BaseFileInput from './BaseFileInput'; -import { BaseFileInputProps } from './file-input-utils'; -import DeleteIcon from '@mui/icons-material/Delete'; +import { ReactNode, useContext, useEffect, useRef, useState } from 'react'; +import { Box, useTheme } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import InputHeader from '../InputHeader'; +import InputFooter from './InputFooter'; +import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext'; +import { isArray } from 'lodash'; +import VideoFileIcon from '@mui/icons-material/VideoFile'; -interface ToolMultipleVideoInputProps - extends Omit { - value: File[] | null; - onChange: (files: File[]) => void; - accept?: string[]; +interface MultiVideoInputComponentProps { + accept: string[]; title?: string; + type: 'video'; + value: MultiVideoInput[]; + onChange: (file: MultiVideoInput[]) => void; +} + +export interface MultiVideoInput { + file: File; + order: number; } export default function ToolMultipleVideoInput({ value, onChange, - accept = ['video/*', '.mkv'], + accept, title, - ...props -}: ToolMultipleVideoInputProps) { - // For preview, use the first file if available - const preview = - value && value.length > 0 ? URL.createObjectURL(value[0]) : undefined; + type +}: MultiVideoInputComponentProps) { + console.log('ToolMultipleVideoInput rendering with value:', value); - // Handler for file selection - const handleFileChange = (file: File | null) => { - if (file) { - // Add to existing files, avoiding duplicates by name - const files = value ? [...value] : []; - if (!files.some((f) => f.name === file.name && f.size === file.size)) { - onChange([...files, file]); - } - } + const fileInputRef = useRef(null); + + const handleFileChange = (event: React.ChangeEvent) => { + const files = event.target.files; + console.log('File change event:', files); + if (files) + onChange([ + ...value, + ...Array.from(files).map((file) => ({ file, order: value.length })) + ]); }; - // Handler for removing a file - const handleRemove = (idx: number) => { - if (!value) return; - const newFiles = value.slice(); - newFiles.splice(idx, 1); - onChange(newFiles); + const handleImportClick = () => { + console.log('Import clicked'); + fileInputRef.current?.click(); + }; + + function handleClear() { + console.log('Clear clicked'); + onChange([]); + } + + function fileNameTruncate(fileName: string) { + const maxLength = 15; + if (fileName.length > maxLength) { + return fileName.slice(0, maxLength) + '...'; + } + return fileName; + } + + const sortList = () => { + const list = [...value]; + list.sort((a, b) => a.order - b.order); + onChange(list); + }; + + const reorderList = (sourceIndex: number, destinationIndex: number) => { + if (destinationIndex === sourceIndex) { + return; + } + const list = [...value]; + + if (destinationIndex === 0) { + list[sourceIndex].order = list[0].order - 1; + sortList(); + return; + } + + if (destinationIndex === list.length - 1) { + list[sourceIndex].order = list[list.length - 1].order + 1; + sortList(); + return; + } + + if (destinationIndex < sourceIndex) { + list[sourceIndex].order = + (list[destinationIndex].order + list[destinationIndex - 1].order) / 2; + sortList(); + return; + } + + list[sourceIndex].order = + (list[destinationIndex].order + list[destinationIndex + 1].order) / 2; + sortList(); }; return ( - - {() => ( + + + - {preview && ( - + + + + + ); } diff --git a/src/pages/tools/video/merge-video/index.tsx b/src/pages/tools/video/merge-video/index.tsx index 11f170b..fb35c0a 100644 --- a/src/pages/tools/video/merge-video/index.tsx +++ b/src/pages/tools/video/merge-video/index.tsx @@ -3,7 +3,9 @@ import React, { useState } from 'react'; import ToolContent from '@components/ToolContent'; import { ToolComponentProps } from '@tools/defineTool'; import ToolFileResult from '@components/result/ToolFileResult'; -import ToolMultipleVideoInput from '@components/input/ToolMultipleVideoInput'; +import ToolMultipleVideoInput, { + MultiVideoInput +} from '@components/input/ToolMultipleVideoInput'; import { main } from './service'; import { InitialValuesType } from './types'; @@ -13,28 +15,42 @@ export default function MergeVideo({ title, longDescription }: ToolComponentProps) { - const [input, setInput] = useState(null); + const [input, setInput] = useState([]); const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); - const compute = async (_values: InitialValuesType, input: File[] | null) => { - if (!input || input.length < 2) return; + console.log('MergeVideo component rendering, input:', input); + + const compute = async ( + _values: InitialValuesType, + input: MultiVideoInput[] + ) => { + console.log('Compute called with input:', input); + if (!input || input.length < 2) { + console.log('Not enough files to merge'); + return; + } setLoading(true); try { - const mergedBlob = await main(input, initialValues); + const files = input.map((item) => item.file); + console.log( + 'Files to merge:', + files.map((f) => f.name) + ); + const mergedBlob = await main(files, initialValues); const mergedFile = new File([mergedBlob], 'merged-video.mp4', { type: 'video/mp4' }); setResult(mergedFile); + console.log('Merge successful'); } catch (err) { + console.error(`Failed to merge video: ${err}`); setResult(null); } finally { setLoading(false); } }; - const getGroups = () => []; - return ( { + console.log('Input changed:', newInput); + setInput(newInput); + }} + accept={['video/*', '.mp4', '.avi', '.mov', '.mkv']} title="Input Videos" + type="video" /> } resultComponent={ @@ -55,7 +76,7 @@ export default function MergeVideo({ /> } initialValues={initialValues} - getGroups={getGroups} + getGroups={null} setInput={setInput} compute={compute} toolInfo={{ title: `What is a ${title}?`, description: longDescription }} diff --git a/src/pages/tools/video/merge-video/merge-video.service.test.ts b/src/pages/tools/video/merge-video/merge-video.service.test.ts index 6efe27d..ee5a8b2 100644 --- a/src/pages/tools/video/merge-video/merge-video.service.test.ts +++ b/src/pages/tools/video/merge-video/merge-video.service.test.ts @@ -1,4 +1,22 @@ -import { expect, describe, it } from 'vitest'; +import { expect, describe, it, vi } from 'vitest'; + +// Mock FFmpeg and fetchFile to avoid Node.js compatibility issues +vi.mock('@ffmpeg/ffmpeg', () => ({ + FFmpeg: vi.fn().mockImplementation(() => ({ + loaded: false, + load: vi.fn().mockResolvedValue(undefined), + writeFile: vi.fn().mockResolvedValue(undefined), + exec: vi.fn().mockResolvedValue(undefined), + readFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3, 4])), + deleteFile: vi.fn().mockResolvedValue(undefined) + })) +})); + +vi.mock('@ffmpeg/util', () => ({ + fetchFile: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3, 4])) +})); + +// Import after mocking import { main } from './service'; function createMockFile(name: string, type = 'video/mp4') { @@ -7,14 +25,28 @@ function createMockFile(name: string, type = 'video/mp4') { describe('merge-video', () => { it('throws if less than two files are provided', async () => { - await expect(main([], {})).rejects.toThrow(); - await expect(main([createMockFile('a.mp4')], {})).rejects.toThrow(); + await expect(main([], {})).rejects.toThrow( + 'Please provide at least two video files to merge.' + ); + await expect(main([createMockFile('a.mp4')], {})).rejects.toThrow( + 'Please provide at least two video files to merge.' + ); }); - it('merges two video files (mocked)', async () => { - // This will throw until ffmpeg logic is implemented - await expect( - main([createMockFile('a.mp4'), createMockFile('b.mp4')], {}) - ).rejects.toThrow('Video merging not yet implemented.'); + it('throws if input is not an array', async () => { + // @ts-ignore - testing invalid input + await expect(main(null, {})).rejects.toThrow( + 'Please provide at least two video files to merge.' + ); + }); + + it('successfully merges video files (mocked)', async () => { + const mockFile1 = createMockFile('video1.mp4'); + const mockFile2 = createMockFile('video2.mp4'); + + const result = await main([mockFile1, mockFile2], {}); + + expect(result).toBeInstanceOf(Blob); + expect(result.type).toBe('video/mp4'); }); }); diff --git a/src/pages/tools/video/merge-video/service.ts b/src/pages/tools/video/merge-video/service.ts index 71569a5..8a374d5 100644 --- a/src/pages/tools/video/merge-video/service.ts +++ b/src/pages/tools/video/merge-video/service.ts @@ -2,8 +2,6 @@ import { InitialValuesType, MergeVideoInput, MergeVideoOutput } from './types'; import { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile } from '@ffmpeg/util'; -const ffmpeg = new FFmpeg(); - // This function will use ffmpeg.wasm to merge multiple video files in the browser. // Returns a Promise that resolves to a Blob of the merged video. export async function main( @@ -14,40 +12,62 @@ export async function main( throw new Error('Please provide at least two video files to merge.'); } - if (!ffmpeg.loaded) { - await ffmpeg.load({ - wasmURL: - 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm' - }); - } + // Create a new FFmpeg instance for each operation to avoid conflicts + const ffmpeg = new FFmpeg(); - // Write all input files to ffmpeg FS - const fileNames = input.map((file, idx) => `input${idx}.mp4`); - for (let i = 0; i < input.length; i++) { - await ffmpeg.writeFile(fileNames[i], await fetchFile(input[i])); - } - - // Create concat list file - const concatList = fileNames.map((name) => `file '${name}'`).join('\n'); - await ffmpeg.writeFile( - 'concat_list.txt', - new TextEncoder().encode(concatList) - ); - - // Run ffmpeg concat demuxer + const fileNames: string[] = []; const outputName = 'output.mp4'; - await ffmpeg.exec([ - '-f', - 'concat', - '-safe', - '0', - '-i', - 'concat_list.txt', - '-c', - 'copy', - outputName - ]); - const mergedData = await ffmpeg.readFile(outputName); - return new Blob([mergedData], { type: 'video/mp4' }); + try { + // Load FFmpeg + if (!ffmpeg.loaded) { + await ffmpeg.load({ + wasmURL: + 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm' + }); + } + + // Write all input files to ffmpeg FS + fileNames.push(...input.map((file, idx) => `input${idx}.mp4`)); + for (let i = 0; i < input.length; i++) { + await ffmpeg.writeFile(fileNames[i], await fetchFile(input[i])); + } + + // Create concat list file + const concatList = fileNames.map((name) => `file '${name}'`).join('\n'); + await ffmpeg.writeFile( + 'concat_list.txt', + new TextEncoder().encode(concatList) + ); + + // Run ffmpeg concat demuxer + await ffmpeg.exec([ + '-f', + 'concat', + '-safe', + '0', + '-i', + 'concat_list.txt', + '-c', + 'copy', + outputName + ]); + + const mergedData = await ffmpeg.readFile(outputName); + return new Blob([mergedData], { type: 'video/mp4' }); + } catch (error) { + console.error('Error merging videos:', error); + throw new Error(`Failed to merge videos: ${error}`); + } finally { + // Clean up temporary files + try { + for (const fileName of fileNames) { + await ffmpeg.deleteFile(fileName); + } + await ffmpeg.deleteFile('concat_list.txt'); + await ffmpeg.deleteFile(outputName); + } catch (cleanupError) { + console.warn('Error cleaning up temporary files:', cleanupError); + } + } } From f730c0548e65ffe2437cb7e1f30a8d3ded604d5f Mon Sep 17 00:00:00 2001 From: AshAnand34 Date: Fri, 11 Jul 2025 15:15:39 -0700 Subject: [PATCH 05/34] Renaming service function to mergeVideos --- src/pages/tools/video/merge-video/index.tsx | 4 ++-- .../video/merge-video/merge-video.service.test.ts | 10 +++++----- src/pages/tools/video/merge-video/service.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/tools/video/merge-video/index.tsx b/src/pages/tools/video/merge-video/index.tsx index fb35c0a..3866b32 100644 --- a/src/pages/tools/video/merge-video/index.tsx +++ b/src/pages/tools/video/merge-video/index.tsx @@ -6,7 +6,7 @@ import ToolFileResult from '@components/result/ToolFileResult'; import ToolMultipleVideoInput, { MultiVideoInput } from '@components/input/ToolMultipleVideoInput'; -import { main } from './service'; +import { mergeVideos } from './service'; import { InitialValuesType } from './types'; const initialValues: InitialValuesType = {}; @@ -37,7 +37,7 @@ export default function MergeVideo({ 'Files to merge:', files.map((f) => f.name) ); - const mergedBlob = await main(files, initialValues); + const mergedBlob = await mergeVideos(files, initialValues); const mergedFile = new File([mergedBlob], 'merged-video.mp4', { type: 'video/mp4' }); diff --git a/src/pages/tools/video/merge-video/merge-video.service.test.ts b/src/pages/tools/video/merge-video/merge-video.service.test.ts index ee5a8b2..ccc81e6 100644 --- a/src/pages/tools/video/merge-video/merge-video.service.test.ts +++ b/src/pages/tools/video/merge-video/merge-video.service.test.ts @@ -17,7 +17,7 @@ vi.mock('@ffmpeg/util', () => ({ })); // Import after mocking -import { main } from './service'; +import { mergeVideos } from './service'; function createMockFile(name: string, type = 'video/mp4') { return new File([new Uint8Array([0, 1, 2])], name, { type }); @@ -25,17 +25,17 @@ function createMockFile(name: string, type = 'video/mp4') { describe('merge-video', () => { it('throws if less than two files are provided', async () => { - await expect(main([], {})).rejects.toThrow( + await expect(mergeVideos([], {})).rejects.toThrow( 'Please provide at least two video files to merge.' ); - await expect(main([createMockFile('a.mp4')], {})).rejects.toThrow( + await expect(mergeVideos([createMockFile('a.mp4')], {})).rejects.toThrow( 'Please provide at least two video files to merge.' ); }); it('throws if input is not an array', async () => { // @ts-ignore - testing invalid input - await expect(main(null, {})).rejects.toThrow( + await expect(mergeVideos(null, {})).rejects.toThrow( 'Please provide at least two video files to merge.' ); }); @@ -44,7 +44,7 @@ describe('merge-video', () => { const mockFile1 = createMockFile('video1.mp4'); const mockFile2 = createMockFile('video2.mp4'); - const result = await main([mockFile1, mockFile2], {}); + const result = await mergeVideos([mockFile1, mockFile2], {}); expect(result).toBeInstanceOf(Blob); expect(result.type).toBe('video/mp4'); diff --git a/src/pages/tools/video/merge-video/service.ts b/src/pages/tools/video/merge-video/service.ts index 8a374d5..c376681 100644 --- a/src/pages/tools/video/merge-video/service.ts +++ b/src/pages/tools/video/merge-video/service.ts @@ -4,7 +4,7 @@ import { fetchFile } from '@ffmpeg/util'; // This function will use ffmpeg.wasm to merge multiple video files in the browser. // Returns a Promise that resolves to a Blob of the merged video. -export async function main( +export async function mergeVideos( input: MergeVideoInput, options: InitialValuesType ): Promise { From 0e09e9e0277fa3d13f976e4a88fc4a39307a67b4 Mon Sep 17 00:00:00 2001 From: Yihao Wang Date: Sun, 13 Jul 2025 22:50:26 +1200 Subject: [PATCH 06/34] Add bookmark helper functions --- src/utils/bookmark.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/utils/bookmark.ts diff --git a/src/utils/bookmark.ts b/src/utils/bookmark.ts new file mode 100644 index 0000000..125e752 --- /dev/null +++ b/src/utils/bookmark.ts @@ -0,0 +1,32 @@ +import { DefinedTool } from '@tools/defineTool'; + +const bookmarkedToolsKey = 'bookmarkedTools'; + +export function getBookmarkedTools(): string[] { + return localStorage.getItem(bookmarkedToolsKey)?.split(',') ?? []; +} + +export function isBookmarked(tool: DefinedTool): boolean { + return getBookmarkedTools().some((path) => path === tool.path); +} + +export function toggleBookmarked(tool: DefinedTool) { + if (isBookmarked(tool)) { + unbookmark(tool); + } else { + bookmark(tool); + } +} + +function bookmark(tool: DefinedTool) { + localStorage.setItem( + bookmarkedToolsKey, + tool.path + localStorage.getItem(bookmarkedToolsKey) + ); +} + +function unbookmark(tool: DefinedTool) { + const bookmarked = localStorage.getItem(bookmarkedToolsKey)?.split(',') ?? []; + const unbookmarked = bookmarked.filter((path) => path != tool.path); + localStorage.setItem(bookmarkedToolsKey, unbookmarked.join(',')); +} From 0d3a17a923629d23faa77a7d808c18695b2cee58 Mon Sep 17 00:00:00 2001 From: Yihao Wang Date: Sun, 13 Jul 2025 23:49:26 +1200 Subject: [PATCH 07/34] Implement bookmarking --- src/components/Hero.tsx | 44 +++++++++++++++++++++++++++++++++++++++-- src/utils/bookmark.ts | 6 +++--- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index c1c6487..e1acc51 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -18,6 +18,11 @@ import { useNavigate } from 'react-router-dom'; import _ from 'lodash'; import { Icon } from '@iconify/react'; import { getToolCategoryTitle } from '@utils/string'; +import { + getBookmarkedToolPaths, + isBookmarked, + toggleBookmarked +} from '@utils/bookmark'; const GroupHeader = styled('div')(({ theme }) => ({ position: 'sticky', @@ -33,7 +38,12 @@ const GroupHeader = styled('div')(({ theme }) => ({ const GroupItems = styled('ul')({ padding: 0 }); -const exampleTools: { label: string; url: string }[] = [ + +type ToolInfo = { + label: string; + url: string; +}; +const exampleTools: ToolInfo[] = [ { label: 'Create a transparent image', url: '/image-generic/create-transparent' @@ -51,6 +61,9 @@ export default function Hero() { const [inputValue, setInputValue] = useState(''); const theme = useTheme(); const [filteredTools, setFilteredTools] = useState(tools); + const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState( + getBookmarkedToolPaths() + ); const navigate = useNavigate(); const handleInputChange = ( event: React.ChangeEvent<{}>, @@ -59,6 +72,20 @@ export default function Hero() { setInputValue(newInputValue); setFilteredTools(filterTools(tools, newInputValue)); }; + const toolsMap = new Map(); + for (const tool of filteredTools) { + toolsMap.set(tool.path, { + label: tool.name, + url: '/' + tool.path + }); + } + + const displayedTools = + bookmarkedToolPaths.length > 0 + ? bookmarkedToolPaths + .map((path) => toolsMap.get(path)) + .filter((tools) => tools != undefined) + : exampleTools; return ( @@ -127,6 +154,19 @@ export default function Hero() { {option.name} {option.shortDescription} + { + e.stopPropagation(); + toggleBookmarked(option); + setBookmarkedToolPaths(getBookmarkedToolPaths()); + }} + icon={ + isBookmarked(option) + ? 'mdi:bookmark' + : 'mdi:bookmark-plus-outline' + } + /> )} @@ -137,7 +177,7 @@ export default function Hero() { }} /> - {exampleTools.map((tool) => ( + {displayedTools.map((tool) => ( navigate(tool.url.startsWith('/') ? tool.url : `/${tool.url}`) diff --git a/src/utils/bookmark.ts b/src/utils/bookmark.ts index 125e752..eb33ea9 100644 --- a/src/utils/bookmark.ts +++ b/src/utils/bookmark.ts @@ -2,12 +2,12 @@ import { DefinedTool } from '@tools/defineTool'; const bookmarkedToolsKey = 'bookmarkedTools'; -export function getBookmarkedTools(): string[] { +export function getBookmarkedToolPaths(): string[] { return localStorage.getItem(bookmarkedToolsKey)?.split(',') ?? []; } export function isBookmarked(tool: DefinedTool): boolean { - return getBookmarkedTools().some((path) => path === tool.path); + return getBookmarkedToolPaths().some((path) => path === tool.path); } export function toggleBookmarked(tool: DefinedTool) { @@ -21,7 +21,7 @@ export function toggleBookmarked(tool: DefinedTool) { function bookmark(tool: DefinedTool) { localStorage.setItem( bookmarkedToolsKey, - tool.path + localStorage.getItem(bookmarkedToolsKey) + tool.path + ',' + (localStorage.getItem(bookmarkedToolsKey) ?? '') ); } From 989aa7958e1b1c95772265923f9e947a43dce075 Mon Sep 17 00:00:00 2001 From: Yihao Wang Date: Mon, 14 Jul 2025 00:11:17 +1200 Subject: [PATCH 08/34] Fix bookmarked tools retrieval --- src/components/Hero.tsx | 21 +++++++++++++++------ src/utils/bookmark.ts | 18 +++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index e1acc51..0508014 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -148,12 +148,21 @@ export default function Hero() { {...props} onClick={() => navigate('/' + option.path)} > - - - - {option.name} - {option.shortDescription} - + + + + + {option.name} + + {option.shortDescription} + + + { diff --git a/src/utils/bookmark.ts b/src/utils/bookmark.ts index eb33ea9..9877973 100644 --- a/src/utils/bookmark.ts +++ b/src/utils/bookmark.ts @@ -3,7 +3,12 @@ import { DefinedTool } from '@tools/defineTool'; const bookmarkedToolsKey = 'bookmarkedTools'; export function getBookmarkedToolPaths(): string[] { - return localStorage.getItem(bookmarkedToolsKey)?.split(',') ?? []; + return ( + localStorage + .getItem(bookmarkedToolsKey) + ?.split(',') + ?.filter((path) => path) ?? [] + ); } export function isBookmarked(tool: DefinedTool): boolean { @@ -21,12 +26,15 @@ export function toggleBookmarked(tool: DefinedTool) { function bookmark(tool: DefinedTool) { localStorage.setItem( bookmarkedToolsKey, - tool.path + ',' + (localStorage.getItem(bookmarkedToolsKey) ?? '') + [tool.path, ...getBookmarkedToolPaths()].join(',') ); } function unbookmark(tool: DefinedTool) { - const bookmarked = localStorage.getItem(bookmarkedToolsKey)?.split(',') ?? []; - const unbookmarked = bookmarked.filter((path) => path != tool.path); - localStorage.setItem(bookmarkedToolsKey, unbookmarked.join(',')); + localStorage.setItem( + bookmarkedToolsKey, + getBookmarkedToolPaths() + .filter((path) => path !== tool.path) + .join(',') + ); } From a50028e1fbe9794e1e73b092679a6f3de34b02f5 Mon Sep 17 00:00:00 2001 From: Yihao Wang Date: Mon, 14 Jul 2025 13:35:48 +1200 Subject: [PATCH 09/34] Improve styling --- src/components/Hero.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 0508014..09f6843 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -151,7 +151,7 @@ export default function Hero() { @@ -170,6 +170,11 @@ export default function Hero() { toggleBookmarked(option); setBookmarkedToolPaths(getBookmarkedToolPaths()); }} + color={ + isBookmarked(option) + ? theme.palette.primary.main + : theme.palette.grey[500] + } icon={ isBookmarked(option) ? 'mdi:bookmark' @@ -185,6 +190,9 @@ export default function Hero() { } }} /> + {bookmarkedToolPaths.length > 0 && ( + Bookmarked tools: + )} {displayedTools.map((tool) => ( Date: Mon, 14 Jul 2025 14:01:54 +1200 Subject: [PATCH 10/34] Add bookmark button to tool page --- src/components/Hero.tsx | 6 +++--- src/components/ToolHeader.tsx | 30 +++++++++++++++++++++++++----- src/components/ToolLayout.tsx | 5 ++++- src/tools/defineTool.tsx | 1 + src/utils/bookmark.ts | 20 ++++++++++---------- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 09f6843..47a4066 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -167,16 +167,16 @@ export default function Hero() { fontSize={20} onClick={(e) => { e.stopPropagation(); - toggleBookmarked(option); + toggleBookmarked(option.path); setBookmarkedToolPaths(getBookmarkedToolPaths()); }} color={ - isBookmarked(option) + isBookmarked(option.path) ? theme.palette.primary.main : theme.palette.grey[500] } icon={ - isBookmarked(option) + isBookmarked(option.path) ? 'mdi:bookmark' : 'mdi:bookmark-plus-outline' } diff --git a/src/components/ToolHeader.tsx b/src/components/ToolHeader.tsx index 8d88f13..29e0bf0 100644 --- a/src/components/ToolHeader.tsx +++ b/src/components/ToolHeader.tsx @@ -1,4 +1,4 @@ -import { Box, Button, styled, useTheme } from '@mui/material'; +import { Box, Button, Stack, styled, useTheme } from '@mui/material'; import Typography from '@mui/material/Typography'; import ToolBreadcrumb from './ToolBreadcrumb'; import { capitalizeFirstLetter } from '../utils/string'; @@ -7,6 +7,7 @@ import { Icon, IconifyIcon } from '@iconify/react'; import { categoriesColors } from '../config/uiConfig'; import { getToolsByCategory } from '@tools/index'; import { useEffect, useState } from 'react'; +import { isBookmarked, toggleBookmarked } from '@utils/bookmark'; const StyledButton = styled(Button)(({ theme }) => ({ backgroundColor: 'white', @@ -21,6 +22,7 @@ interface ToolHeaderProps { description: string; icon?: IconifyIcon | string; type: string; + path: string; } function ToolLinks() { @@ -80,8 +82,11 @@ export default function ToolHeader({ icon, title, description, - type + type, + path }: ToolHeaderProps) { + const theme = useTheme(); + const [bookmarked, setBookmarked] = useState(isBookmarked(path)); return ( - - {title} - + + + {title} + + { + toggleBookmarked(path); + setBookmarked(!bookmarked); + }} + icon={bookmarked ? 'mdi:bookmark' : 'mdi:bookmark-plus-outline'} + /> + {description} diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx index 3da0837..6d843f6 100644 --- a/src/components/ToolLayout.tsx +++ b/src/components/ToolLayout.tsx @@ -13,12 +13,14 @@ export default function ToolLayout({ title, description, icon, - type + type, + path }: { title: string; description: string; icon?: IconifyIcon | string; type: string; + path: string; children: ReactNode; }) { const otherCategoryTools = @@ -49,6 +51,7 @@ export default function ToolLayout({ description={description} icon={icon} type={type} + path={path} /> {children} diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx index ecb056c..1d0a605 100644 --- a/src/tools/defineTool.tsx +++ b/src/tools/defineTool.tsx @@ -74,6 +74,7 @@ export const defineTool = ( description={description} icon={icon} type={basePath} + path={`${basePath}/${path}`} > diff --git a/src/utils/bookmark.ts b/src/utils/bookmark.ts index 9877973..a63166c 100644 --- a/src/utils/bookmark.ts +++ b/src/utils/bookmark.ts @@ -11,30 +11,30 @@ export function getBookmarkedToolPaths(): string[] { ); } -export function isBookmarked(tool: DefinedTool): boolean { - return getBookmarkedToolPaths().some((path) => path === tool.path); +export function isBookmarked(toolPath: string): boolean { + return getBookmarkedToolPaths().some((path) => path === toolPath); } -export function toggleBookmarked(tool: DefinedTool) { - if (isBookmarked(tool)) { - unbookmark(tool); +export function toggleBookmarked(toolPath: string) { + if (isBookmarked(toolPath)) { + unbookmark(toolPath); } else { - bookmark(tool); + bookmark(toolPath); } } -function bookmark(tool: DefinedTool) { +function bookmark(toolPath: string) { localStorage.setItem( bookmarkedToolsKey, - [tool.path, ...getBookmarkedToolPaths()].join(',') + [toolPath, ...getBookmarkedToolPaths()].join(',') ); } -function unbookmark(tool: DefinedTool) { +function unbookmark(toolPath: string) { localStorage.setItem( bookmarkedToolsKey, getBookmarkedToolPaths() - .filter((path) => path !== tool.path) + .filter((path) => path !== toolPath) .join(',') ); } From 5b98dda19ba983c75a1f44ea4660e49e6f79a12e Mon Sep 17 00:00:00 2001 From: Yihao Wang Date: Mon, 14 Jul 2025 14:13:52 +1200 Subject: [PATCH 11/34] Allow unbookmarking from the card --- src/components/Hero.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 47a4066..9845b00 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -222,7 +222,22 @@ export default function Hero() { } }} > - {tool.label} + + {tool.label} + {bookmarkedToolPaths.length > 0 && ( + { + e.stopPropagation(); + const path = tool.url.substring(1); + toggleBookmarked(path); + setBookmarkedToolPaths(getBookmarkedToolPaths()); + }} + /> + )} + ))} From 911057d56075b493176f234ac2dc25d9d0c34a49 Mon Sep 17 00:00:00 2001 From: Yihao Wang Date: Mon, 14 Jul 2025 14:18:15 +1200 Subject: [PATCH 12/34] Clean up --- src/utils/bookmark.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/bookmark.ts b/src/utils/bookmark.ts index a63166c..859d6e9 100644 --- a/src/utils/bookmark.ts +++ b/src/utils/bookmark.ts @@ -1,5 +1,3 @@ -import { DefinedTool } from '@tools/defineTool'; - const bookmarkedToolsKey = 'bookmarkedTools'; export function getBookmarkedToolPaths(): string[] { From afc61e6f4c03871070ce788451baf04f7b03b516 Mon Sep 17 00:00:00 2001 From: Yihao Wang Date: Mon, 14 Jul 2025 19:14:20 +1200 Subject: [PATCH 13/34] Fix typing --- src/components/Hero.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 9845b00..a05499d 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -82,9 +82,13 @@ export default function Hero() { const displayedTools = bookmarkedToolPaths.length > 0 - ? bookmarkedToolPaths - .map((path) => toolsMap.get(path)) - .filter((tools) => tools != undefined) + ? bookmarkedToolPaths.flatMap((path) => { + const tool = toolsMap.get(path); + if (tool === undefined) { + return []; + } + return [tool]; + }) : exampleTools; return ( From 1031db7ed2cb4c75882b94565afbd4b7fcec2814 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 15 Jul 2025 14:01:38 +0100 Subject: [PATCH 14/34] chore: refine bookmarking --- src/components/Hero.tsx | 54 +++++++++++++++++++---------------- src/components/ToolHeader.tsx | 22 ++++++++------ src/components/ToolLayout.tsx | 6 ++-- src/tools/defineTool.tsx | 2 +- 4 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index a05499d..9a2ebe0 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -23,6 +23,7 @@ import { isBookmarked, toggleBookmarked } from '@utils/bookmark'; +import IconButton from '@mui/material/IconButton'; const GroupHeader = styled('div')(({ theme }) => ({ position: 'sticky', @@ -167,24 +168,27 @@ export default function Hero() { - { e.stopPropagation(); toggleBookmarked(option.path); setBookmarkedToolPaths(getBookmarkedToolPaths()); }} - color={ - isBookmarked(option.path) - ? theme.palette.primary.main - : theme.palette.grey[500] - } - icon={ - isBookmarked(option.path) - ? 'mdi:bookmark' - : 'mdi:bookmark-plus-outline' - } - /> + > + + )} @@ -194,9 +198,6 @@ export default function Hero() { } }} /> - {bookmarkedToolPaths.length > 0 && ( - Bookmarked tools: - )} {displayedTools.map((tool) => ( - - {tool.label} + + {tool.label} {bookmarkedToolPaths.length > 0 && ( - { e.stopPropagation(); const path = tool.url.substring(1); toggleBookmarked(path); setBookmarkedToolPaths(getBookmarkedToolPaths()); }} - /> + size={'small'} + > + + )} diff --git a/src/components/ToolHeader.tsx b/src/components/ToolHeader.tsx index 29e0bf0..fa24afb 100644 --- a/src/components/ToolHeader.tsx +++ b/src/components/ToolHeader.tsx @@ -8,6 +8,7 @@ import { categoriesColors } from '../config/uiConfig'; import { getToolsByCategory } from '@tools/index'; import { useEffect, useState } from 'react'; import { isBookmarked, toggleBookmarked } from '@utils/bookmark'; +import IconButton from '@mui/material/IconButton'; const StyledButton = styled(Button)(({ theme }) => ({ backgroundColor: 'white', @@ -107,19 +108,22 @@ export default function ToolHeader({ {title} - { toggleBookmarked(path); setBookmarked(!bookmarked); }} - icon={bookmarked ? 'mdi:bookmark' : 'mdi:bookmark-plus-outline'} - /> + > + + {description} diff --git a/src/components/ToolLayout.tsx b/src/components/ToolLayout.tsx index 6d843f6..1b2e50b 100644 --- a/src/components/ToolLayout.tsx +++ b/src/components/ToolLayout.tsx @@ -14,13 +14,13 @@ export default function ToolLayout({ description, icon, type, - path + fullPath }: { title: string; description: string; icon?: IconifyIcon | string; type: string; - path: string; + fullPath: string; children: ReactNode; }) { const otherCategoryTools = @@ -51,7 +51,7 @@ export default function ToolLayout({ description={description} icon={icon} type={type} - path={path} + path={fullPath} /> {children} diff --git a/src/tools/defineTool.tsx b/src/tools/defineTool.tsx index 1d0a605..6439839 100644 --- a/src/tools/defineTool.tsx +++ b/src/tools/defineTool.tsx @@ -74,7 +74,7 @@ export const defineTool = ( description={description} icon={icon} type={basePath} - path={`${basePath}/${path}`} + fullPath={`${basePath}/${path}`} > From 67e092ff1cdcf4a4d3313b6d298997f6bb25d364 Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 15 Jul 2025 14:15:46 +0100 Subject: [PATCH 15/34] fix: translations --- src/components/Hero.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index ba2bc9d..4cae00e 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -117,7 +117,7 @@ export default function Hero() { if (tool === undefined) { return []; } - return [tool]; + return [{ ...tool, label: t(tool.label) }]; }) : exampleTools; @@ -189,9 +189,9 @@ export default function Hero() { - {option.name} + {t(option.name)} - {option.shortDescription} + {t(option.shortDescription)} From aed1681b7fa6c67a4d4e57c82b10d1a4fe39f47e Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 15 Jul 2025 14:23:44 +0100 Subject: [PATCH 16/34] chore: delete unused i18n json files --- .idea/workspace.xml | 250 ++++++++++++++------------- src/i18n/en.json | 254 ---------------------------- src/i18n/hi.json | 254 ---------------------------- src/pages/tools/audio/i18n/en.json | 43 ----- src/pages/tools/audio/i18n/hi.json | 64 ------- src/pages/tools/csv/i18n/en.json | 114 ------------- src/pages/tools/csv/i18n/hi.json | 123 -------------- src/pages/tools/image/i18n/en.json | 98 ----------- src/pages/tools/image/i18n/hi.json | 152 ----------------- src/pages/tools/json/i18n/en.json | 65 ------- src/pages/tools/json/i18n/hi.json | 80 --------- src/pages/tools/list/i18n/en.json | 211 ----------------------- src/pages/tools/list/i18n/hi.json | 138 --------------- src/pages/tools/number/i18n/en.json | 91 ---------- src/pages/tools/number/i18n/hi.json | 45 ----- src/pages/tools/pdf/i18n/en.json | 113 ------------- src/pages/tools/pdf/i18n/hi.json | 82 --------- src/pages/tools/string/i18n/en.json | 243 -------------------------- src/pages/tools/string/i18n/hi.json | 196 --------------------- src/pages/tools/time/i18n/en.json | 106 ------------ src/pages/tools/time/i18n/hi.json | 105 ------------ src/pages/tools/video/i18n/en.json | 115 ------------- src/pages/tools/video/i18n/hi.json | 140 --------------- src/pages/tools/xml/i18n/en.json | 40 ----- src/pages/tools/xml/i18n/hi.json | 42 ----- 25 files changed, 131 insertions(+), 3033 deletions(-) delete mode 100644 src/i18n/en.json delete mode 100644 src/i18n/hi.json delete mode 100644 src/pages/tools/audio/i18n/en.json delete mode 100644 src/pages/tools/audio/i18n/hi.json delete mode 100644 src/pages/tools/csv/i18n/en.json delete mode 100644 src/pages/tools/csv/i18n/hi.json delete mode 100644 src/pages/tools/image/i18n/en.json delete mode 100644 src/pages/tools/image/i18n/hi.json delete mode 100644 src/pages/tools/json/i18n/en.json delete mode 100644 src/pages/tools/json/i18n/hi.json delete mode 100644 src/pages/tools/list/i18n/en.json delete mode 100644 src/pages/tools/list/i18n/hi.json delete mode 100644 src/pages/tools/number/i18n/en.json delete mode 100644 src/pages/tools/number/i18n/hi.json delete mode 100644 src/pages/tools/pdf/i18n/en.json delete mode 100644 src/pages/tools/pdf/i18n/hi.json delete mode 100644 src/pages/tools/string/i18n/en.json delete mode 100644 src/pages/tools/string/i18n/hi.json delete mode 100644 src/pages/tools/time/i18n/en.json delete mode 100644 src/pages/tools/time/i18n/hi.json delete mode 100644 src/pages/tools/video/i18n/en.json delete mode 100644 src/pages/tools/video/i18n/hi.json delete mode 100644 src/pages/tools/xml/i18n/en.json delete mode 100644 src/pages/tools/xml/i18n/hi.json diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 7c01f00..bc70dd6 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,17 +4,22 @@ - + + - - - + + + @@ -22,7 +27,7 @@ +): { title: string; rawTitle: string; description: string; @@ -179,9 +179,13 @@ export const getToolsByCategory = (): { (config) => config.type === type ); return { - rawTitle: categoryConfig?.title ?? capitalizeFirstLetter(type), - title: `${categoryConfig?.title ?? capitalizeFirstLetter(type)} Tools`, - description: categoryConfig?.value ?? '', + rawTitle: categoryConfig?.title + ? t(categoryConfig.title) + : capitalizeFirstLetter(type), + title: categoryConfig?.title + ? t(categoryConfig.title) + : `${capitalizeFirstLetter(type)} Tools`, + description: categoryConfig?.value ? t(categoryConfig.value) : '', type, icon: categoryConfig!.icon, tools: tools ?? [], diff --git a/src/utils/string.ts b/src/utils/string.ts index f145cdd..4207348 100644 --- a/src/utils/string.ts +++ b/src/utils/string.ts @@ -2,6 +2,7 @@ import { UpdateField } from '@components/options/ToolOptions'; import { getToolsByCategory } from '@tools/index'; import { ToolCategory } from '@tools/defineTool'; import { I18nNamespaces, validNamespaces } from '../i18n'; +import { TFunction } from 'i18next'; // Here starting the shared values for string manipulation. @@ -109,8 +110,11 @@ export function itemCounter( return dict; } -export const getToolCategoryTitle = (categoryName: string): string => - getToolsByCategory().find((category) => category.type === categoryName)! +export const getToolCategoryTitle = ( + categoryName: string, + t: TFunction +): string => + getToolsByCategory(t).find((category) => category.type === categoryName)! .rawTitle; // Type guard to check if a value is a valid I18nNamespaces From f5103a3117b0bd471003e9a1ff0a853f38e3667d Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 15 Jul 2025 18:38:31 +0100 Subject: [PATCH 21/34] fix: i18n --- public/locales/en/translation.json | 4 ---- public/locales/fr/translation.json | 8 ++------ public/locales/hi/translation.json | 6 +----- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 71fadf9..789e161 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -1,8 +1,4 @@ { - "app": { - "language": "Language", - "title": "Omni Tools" - }, "audio": { "changeSpeed": { "description": "Change the playback speed of audio files. Speed up or slow down audio while maintaining pitch.", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index ce5493f..cc8c522 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -1,8 +1,4 @@ { - "app": { - "language": "Langue", - "title": "OmniTools" - }, "audio": { "changeSpeed": { "description": "Modifier la vitesse de lecture des fichiers audio. Accélérer ou ralentir le son tout en conservant la hauteur.", @@ -58,7 +54,7 @@ "description": "Outils pour travailler avec des images PNG : convertissez des PNG en JPG, créez des PNG transparents, modifiez les couleurs PNG, recadrez, faites pivoter, redimensionnez des PNG et bien plus encore.", "title": "Outils PNG" }, - "seeAll": "Voir les {{title}}", + "seeAll": "Tout voir {{title}}", "string": { "description": "Outils pour travailler avec du texte : convertissez du texte en images, recherchez et remplacez du texte, divisez du texte en fragments, joignez des lignes de texte, répétez du texte et bien plus encore.", "title": "Outils de texte" @@ -220,7 +216,7 @@ "seeExamples": "Voir des exemples" }, "toolLayout": { - "allToolsTitle": "Tous les {{type}}" + "allToolsTitle": "Tous {{type}} Outils" }, "toolMultiFileResult": { "copied": "Fichier copié", diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index ae00805..3eba1bb 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -1,8 +1,4 @@ { - "app": { - "language": "भाषा", - "title": "ओमनी टूल्स" - }, "audio": { "changeSpeed": { "description": "ऑडियो फ़ाइलों की प्लेबैक गति बदलें। पिच बनाए रखते हुए ऑडियो को तेज़ या धीमा करें।", @@ -220,7 +216,7 @@ "seeExamples": "उदाहरण देखें" }, "toolLayout": { - "allToolsTitle": "सभी {{type}}" + "allToolsTitle": "सभी {{type}} टूल्स" }, "toolMultiFileResult": { "copied": "फ़ाइल कॉपी की गई", From 1838a13163f0bbd782f8a01eee59bc53d84f8fdb Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 15 Jul 2025 18:45:02 +0100 Subject: [PATCH 22/34] chore: remove prebuild --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 32edb2d..0806cad 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ }, "scripts": { "dev": "vite", - "prebuild": "npm run i18n:pull", "build": "tsc && vite build", "serve": "vite preview", "test": "vitest", From 11b9bf80079ed360178a74fed4bde0a0b035eefc Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 15 Jul 2025 18:53:03 +0100 Subject: [PATCH 23/34] fix: broken translations --- package.json | 2 +- public/locales/fr/translation.json | 4 ++-- public/locales/hi/translation.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 0806cad..1671e84 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "typecheck": "tsc --project tsconfig.json --noEmit", "prepare": "husky install", "i18n:pull": "locize download --project-id e7156a3e-66fb-4035-a0f0-cebf1c63a3ba --path ./public/locales", - "i18n:sync": "locize sync --project-id e7156a3e-66fb-4035-a0f0-cebf1c63a3ba --path ./public/locales --update-values true" + "i18n:sync": "locize sync --project-id e7156a3e-66fb-4035-a0f0-cebf1c63a3ba --path ./public/locales --update-values true --namespace de,en,es,fr,hi,ja,nl,pt,ru,zh" }, "dependencies": { "@emotion/react": "^11.11.4", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index cc8c522..3257c16 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -54,7 +54,7 @@ "description": "Outils pour travailler avec des images PNG : convertissez des PNG en JPG, créez des PNG transparents, modifiez les couleurs PNG, recadrez, faites pivoter, redimensionnez des PNG et bien plus encore.", "title": "Outils PNG" }, - "seeAll": "Tout voir {{title}}", + "seeAll": "Voir les {{title}}", "string": { "description": "Outils pour travailler avec du texte : convertissez du texte en images, recherchez et remplacez du texte, divisez du texte en fragments, joignez des lignes de texte, répétez du texte et bien plus encore.", "title": "Outils de texte" @@ -216,7 +216,7 @@ "seeExamples": "Voir des exemples" }, "toolLayout": { - "allToolsTitle": "Tous {{type}} Outils" + "allToolsTitle": "Tous les {{type}}" }, "toolMultiFileResult": { "copied": "Fichier copié", diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index 3eba1bb..84b4bf0 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -216,7 +216,7 @@ "seeExamples": "उदाहरण देखें" }, "toolLayout": { - "allToolsTitle": "सभी {{type}} टूल्स" + "allToolsTitle": "सभी {{type}}" }, "toolMultiFileResult": { "copied": "फ़ाइल कॉपी की गई", From a0bb24c5209bffada863337f1f240a640028e21b Mon Sep 17 00:00:00 2001 From: "Ibrahima G. Coulibaly" Date: Tue, 15 Jul 2025 18:57:21 +0100 Subject: [PATCH 24/34] fix: i18n sync --- .idea/workspace.xml | 410 ++++++++++++++--------------- package.json | 2 +- public/locales/fr/translation.json | 4 +- public/locales/hi/translation.json | 2 +- 4 files changed, 204 insertions(+), 214 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 8c4e45d..2f2582e 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,19 +4,9 @@