Merge branch 'main' of https://github.com/iib0011/omni-tools into fork/AshAnand34/merge-video-tool

This commit is contained in:
Ibrahima G. Coulibaly
2025-07-18 02:35:35 +01:00
323 changed files with 20828 additions and 2044 deletions

View File

@@ -10,6 +10,8 @@ import { tools } from '../tools';
import './index.css';
import { darkTheme, lightTheme } from '../config/muiConfig';
import ScrollToTopButton from './ScrollToTopButton';
import { I18nextProvider } from 'react-i18next';
import i18n from '../i18n';
export type Mode = 'dark' | 'light' | 'system';
@@ -44,32 +46,34 @@ function App() {
}, []);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<SnackbarProvider
maxSnack={5}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
>
<CustomSnackBarProvider>
<BrowserRouter>
<Navbar
mode={mode}
onChangeMode={() => {
setMode((prev) => nextMode(prev));
localStorage.setItem('theme', nextMode(mode));
}}
/>
<Suspense fallback={<Loading />}>
<AppRoutes />
</Suspense>
</BrowserRouter>
</CustomSnackBarProvider>
</SnackbarProvider>
<ScrollToTopButton />
</ThemeProvider>
<I18nextProvider i18n={i18n}>
<ThemeProvider theme={theme}>
<CssBaseline />
<SnackbarProvider
maxSnack={5}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
>
<CustomSnackBarProvider>
<BrowserRouter>
<Navbar
mode={mode}
onChangeMode={() => {
setMode((prev) => nextMode(prev));
localStorage.setItem('theme', nextMode(mode));
}}
/>
<Suspense fallback={<Loading />}>
<AppRoutes />
</Suspense>
</BrowserRouter>
</CustomSnackBarProvider>
</SnackbarProvider>
<ScrollToTopButton />
</ThemeProvider>
</I18nextProvider>
);
}

View File

@@ -15,9 +15,16 @@ import { useState } from 'react';
import { DefinedTool } from '@tools/defineTool';
import { filterTools, tools } from '@tools/index';
import { useNavigate } from 'react-router-dom';
import _ from 'lodash';
import { Icon } from '@iconify/react';
import { getToolCategoryTitle } from '@utils/string';
import { useTranslation } from 'react-i18next';
import { FullI18nKey, validNamespaces } from '../i18n';
import {
getBookmarkedToolPaths,
isBookmarked,
toggleBookmarked
} from '@utils/bookmark';
import IconButton from '@mui/material/IconButton';
const GroupHeader = styled('div')(({ theme }) => ({
position: 'sticky',
@@ -33,44 +40,98 @@ const GroupHeader = styled('div')(({ theme }) => ({
const GroupItems = styled('ul')({
padding: 0
});
const exampleTools: { label: string; url: string }[] = [
{
label: 'Create a transparent image',
url: '/image-generic/create-transparent'
},
{ label: 'Prettify JSON', url: '/json/prettify' },
{ label: 'Change GIF speed', url: '/gif/change-speed' },
{ label: 'Sort a list', url: '/list/sort' },
{ label: 'Compress PNG', url: '/png/compress-png' },
{ label: 'Split a text', url: '/string/split' },
{ label: 'Split PDF', url: '/pdf/split-pdf' },
{ label: 'Trim video', url: '/video/trim' },
{ label: 'Calculate number sum', url: '/number/sum' }
];
type ToolInfo = {
label: FullI18nKey;
url: string;
};
export default function Hero() {
const { t } = useTranslation(validNamespaces);
const [inputValue, setInputValue] = useState<string>('');
const theme = useTheme();
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState<string[]>(
getBookmarkedToolPaths()
);
const navigate = useNavigate();
const exampleTools: ToolInfo[] = [
{
label: 'translation:hero.examples.createTransparentImage',
url: '/image-generic/create-transparent'
},
{
label: 'translation:hero.examples.prettifyJson',
url: '/json/prettify'
},
{
label: 'translation:hero.examples.changeGifSpeed',
url: '/gif/change-speed'
},
{
label: 'translation:hero.examples.sortList',
url: '/list/sort'
},
{
label: 'translation:hero.examples.compressPng',
url: '/png/compress-png'
},
{
label: 'translation:hero.examples.splitText',
url: '/string/split'
},
{
label: 'translation:hero.examples.splitPdf',
url: '/pdf/split-pdf'
},
{
label: 'translation:hero.examples.trimVideo',
url: '/video/trim'
},
{
label: 'translation:hero.examples.calculateNumberSum',
url: '/number/sum'
}
];
const handleInputChange = (
event: React.ChangeEvent<{}>,
newInputValue: string
) => {
setInputValue(newInputValue);
setFilteredTools(filterTools(tools, newInputValue));
setFilteredTools(filterTools(tools, newInputValue, t));
};
const toolsMap = new Map<string, ToolInfo>();
for (const tool of filteredTools) {
toolsMap.set(tool.path, {
label: tool.name,
url: '/' + tool.path
});
}
const displayedTools =
bookmarkedToolPaths.length > 0
? bookmarkedToolPaths.flatMap((path) => {
const tool = toolsMap.get(path);
if (tool === undefined) {
return [];
}
return [tool];
})
: exampleTools;
return (
<Box width={{ xs: '90%', md: '80%', lg: '60%' }}>
<Stack mb={1} direction={'row'} spacing={1} justifyContent={'center'}>
<Typography sx={{ textAlign: 'center' }} fontSize={{ xs: 25, md: 30 }}>
Get Things Done Quickly with{' '}
{t('translation:hero.title')}{' '}
<Typography
fontSize={{ xs: 25, md: 30 }}
display={'inline'}
color={'primary'}
>
OmniTools
{t('translation:hero.brand')}
</Typography>
</Typography>
</Stack>
@@ -79,9 +140,7 @@ export default function Hero() {
fontSize={{ xs: 15, md: 20 }}
mb={2}
>
Boost your productivity with OmniTools, the ultimate toolkit for getting
things done quickly! Access thousands of user-friendly utilities for
editing images, text, lists, and data, all directly from your browser.
{t('translation:hero.description')}
</Typography>
<Autocomplete
@@ -92,18 +151,18 @@ export default function Hero() {
renderGroup={(params) => {
return (
<li key={params.key}>
<GroupHeader>{getToolCategoryTitle(params.group)}</GroupHeader>
<GroupHeader>{getToolCategoryTitle(params.group, t)}</GroupHeader>
<GroupItems>{params.children}</GroupItems>
</li>
);
}}
inputValue={inputValue}
getOptionLabel={(option) => option.name}
getOptionLabel={(option) => t(option.name)}
renderInput={(params) => (
<TextField
{...params}
fullWidth
placeholder={'Search all tools'}
placeholder={t('translation:hero.searchPlaceholder')}
InputProps={{
...params.InputProps,
endAdornment: <SearchIcon />,
@@ -121,12 +180,42 @@ export default function Hero() {
{...props}
onClick={() => navigate('/' + option.path)}
>
<Stack direction={'row'} spacing={2} alignItems={'center'}>
<Icon fontSize={20} icon={option.icon} />
<Box>
<Typography fontWeight={'bold'}>{option.name}</Typography>
<Typography fontSize={12}>{option.shortDescription}</Typography>
</Box>
<Stack
direction={'row'}
alignItems={'center'}
justifyContent={'space-between'}
width={'100%'}
>
<Stack direction={'row'} spacing={2} alignItems={'center'}>
<Icon fontSize={20} icon={option.icon} />
<Box>
<Typography fontWeight={'bold'}>{t(option.name)}</Typography>
<Typography fontSize={12}>
{t(option.shortDescription)}
</Typography>
</Box>
</Stack>
<IconButton
onClick={(e) => {
e.stopPropagation();
toggleBookmarked(option.path);
setBookmarkedToolPaths(getBookmarkedToolPaths());
}}
>
<Icon
fontSize={20}
color={
isBookmarked(option.path)
? theme.palette.primary.main
: theme.palette.grey[500]
}
icon={
isBookmarked(option.path)
? 'mdi:bookmark'
: 'mdi:bookmark-plus-outline'
}
/>
</IconButton>
</Stack>
</Box>
)}
@@ -137,7 +226,7 @@ export default function Hero() {
}}
/>
<Grid container spacing={2} mt={2}>
{exampleTools.map((tool) => (
{displayedTools.map((tool) => (
<Grid
onClick={() =>
navigate(tool.url.startsWith('/') ? tool.url : `/${tool.url}`)
@@ -162,10 +251,30 @@ export default function Hero() {
cursor: 'pointer',
'&:hover': {
backgroundColor: 'background.hover'
}
},
height: '100%'
}}
>
<Typography>{tool.label}</Typography>
<Stack direction={'row'} spacing={1} alignItems={'center'}>
<Typography textAlign={'center'}>{t(tool.label)}</Typography>
{bookmarkedToolPaths.length > 0 && (
<IconButton
onClick={(e) => {
e.stopPropagation();
const path = tool.url.substring(1);
toggleBookmarked(path);
setBookmarkedToolPaths(getBookmarkedToolPaths());
}}
size={'small'}
>
<Icon
icon={'mdi:close'}
color={theme.palette.grey[500]}
fontSize={15}
/>
</IconButton>
)}
</Stack>
</Box>
</Grid>
))}

View File

@@ -13,22 +13,39 @@ import {
ListItem,
ListItemButton,
ListItemText,
Stack
Stack,
Select,
MenuItem,
FormControl
} from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import { Icon } from '@iconify/react';
import { Mode } from 'components/App';
import { useTranslation } from 'react-i18next';
interface NavbarProps {
mode: Mode;
onChangeMode: () => void;
}
const languages = [
{ code: 'en', label: 'English' },
{ code: 'de', label: 'Deutsch' },
{ code: 'es', label: 'Español' },
{ code: 'fr', label: 'Français' },
{ code: 'pt', label: 'Português' },
{ code: 'ja', label: '日本語' },
{ code: 'hi', label: 'हिंदी' },
{ code: 'nl', label: 'Nederlands' },
{ code: 'ru', label: 'Русский' },
{ code: 'zh', label: '中文' }
];
const Navbar: React.FC<NavbarProps> = ({
mode,
onChangeMode: onChangeMode
}) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@@ -36,12 +53,51 @@ const Navbar: React.FC<NavbarProps> = ({
const toggleDrawer = (open: boolean) => () => {
setDrawerOpen(open);
};
const handleLanguageChange = (event: any) => {
const newLanguage = event.target.value;
i18n.changeLanguage(newLanguage);
localStorage.setItem('lang', newLanguage);
};
const navItems: { label: string; path: string }[] = [
// { label: 'Features', path: '/features' }
// { label: 'About Us', path: '/about-us' }
];
const languageSelector = (
<FormControl size="small" sx={{ minWidth: 120 }}>
<Select
value={i18n.language}
onChange={handleLanguageChange}
displayEmpty
sx={{
color: 'inherit',
'& .MuiSelect-icon': {
color: 'inherit'
},
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'transparent'
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: 'transparent'
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: 'transparent'
}
}}
>
{languages.map((lang) => (
<MenuItem key={lang.code} value={lang.code}>
{lang.label}
</MenuItem>
))}
</Select>
</FormControl>
);
const buttons: ReactNode[] = [
languageSelector,
<Icon
key={mode}
onClick={onChangeMode}
@@ -83,7 +139,7 @@ const Navbar: React.FC<NavbarProps> = ({
/>
}
>
Buy me a coffee
{t('navbar.buyMeACoffee')}
</Button>
];
const drawerList = (

View File

@@ -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,11 @@ 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';
import IconButton from '@mui/material/IconButton';
import { useTranslation } from 'react-i18next';
import useMediaQuery from '@mui/material/useMediaQuery';
import { validNamespaces } from '../i18n';
const StyledButton = styled(Button)(({ theme }) => ({
backgroundColor: 'white',
@@ -21,10 +26,14 @@ interface ToolHeaderProps {
description: string;
icon?: IconifyIcon | string;
type: string;
path: string;
}
function ToolLinks() {
const { t } = useTranslation();
const [examplesVisible, setExamplesVisible] = useState(false);
const theme = useTheme();
const isMd = useMediaQuery(theme.breakpoints.down('md'));
useEffect(() => {
const timeout = setTimeout(() => {
@@ -45,16 +54,18 @@ function ToolLinks() {
}
return (
<Grid container spacing={2} mt={1}>
<Grid item md={12} lg={6}>
<StyledButton
sx={{ backgroundColor: 'background.paper' }}
fullWidth
variant="outlined"
onClick={() => scrollToElement('tool')}
>
Use This Tool
</StyledButton>
</Grid>
{isMd && (
<Grid item md={12} lg={6}>
<StyledButton
sx={{ backgroundColor: 'background.paper' }}
fullWidth
variant="outlined"
onClick={() => scrollToElement('tool')}
>
Use This Tool
</StyledButton>
</Grid>
)}
{examplesVisible && (
<Grid item md={12} lg={6}>
<StyledButton
@@ -63,7 +74,7 @@ function ToolLinks() {
sx={{ backgroundColor: 'background.paper' }}
onClick={() => scrollToElement('examples')}
>
See Examples
{t('toolHeader.seeExamples')}
</StyledButton>
</Grid>
)}
@@ -80,15 +91,19 @@ export default function ToolHeader({
icon,
title,
description,
type
type,
path
}: ToolHeaderProps) {
const theme = useTheme();
const { t } = useTranslation();
const [bookmarked, setBookmarked] = useState<boolean>(isBookmarked(path));
return (
<Box my={4}>
<ToolBreadcrumb
items={[
{ title: 'All tools', link: '/' },
{
title: getToolsByCategory().find(
title: getToolsByCategory(t).find(
(category) => category.type === type
)!.rawTitle,
link: '/categories/' + type
@@ -98,9 +113,27 @@ export default function ToolHeader({
/>
<Grid mt={1} container spacing={2}>
<Grid item xs={12} md={8}>
<Typography mb={2} fontSize={30} color={'primary'}>
{title}
</Typography>
<Stack direction={'row'} spacing={2} alignItems={'center'}>
<Typography mb={2} fontSize={30} color={'primary'}>
{title}
</Typography>
<IconButton
onClick={(e) => {
toggleBookmarked(path);
setBookmarked(!bookmarked);
}}
>
<Icon
fontSize={30}
color={
bookmarked
? theme.palette.primary.main
: theme.palette.grey[500]
}
icon={bookmarked ? 'mdi:bookmark' : 'mdi:bookmark-plus-outline'}
/>
</IconButton>
</Stack>
<Typography fontSize={20}>{description}</Typography>
<ToolLinks />
</Grid>

View File

@@ -5,26 +5,47 @@ import ToolHeader from './ToolHeader';
import Separator from './Separator';
import AllTools from './allTools/AllTools';
import { getToolsByCategory } from '@tools/index';
import { capitalizeFirstLetter } from '../utils/string';
import {
capitalizeFirstLetter,
getI18nNamespaceFromToolCategory
} from '../utils/string';
import { IconifyIcon } from '@iconify/react';
import { useTranslation } from 'react-i18next';
import { ToolCategory } from '@tools/defineTool';
import { FullI18nKey } from '../i18n';
export default function ToolLayout({
children,
title,
description,
icon,
type
i18n,
type,
fullPath
}: {
title: string;
description: string;
icon?: IconifyIcon | string;
type: string;
type: ToolCategory;
fullPath: string;
children: ReactNode;
i18n?: {
name: FullI18nKey;
description: FullI18nKey;
shortDescription: FullI18nKey;
};
}) {
const { t } = useTranslation([
'translation',
getI18nNamespaceFromToolCategory(type)
]);
// Use i18n keys if available, otherwise fall back to provided strings
//@ts-ignore
const toolTitle: string = t(i18n.name);
//@ts-ignore
const toolDescription: string = t(i18n.description);
const otherCategoryTools =
getToolsByCategory()
getToolsByCategory(t)
.find((category) => category.type === type)
?.tools.filter((tool) => tool.name !== title)
?.tools.filter((tool) => t(tool.name) !== toolTitle)
.map((tool) => ({
title: tool.name,
description: tool.shortDescription,
@@ -41,22 +62,25 @@ export default function ToolLayout({
sx={{ backgroundColor: 'background.default' }}
>
<Helmet>
<title>{`${title} - OmniTools`}</title>
<title>{`${toolTitle} - OmniTools`}</title>
</Helmet>
<Box width={'85%'}>
<ToolHeader
title={title}
description={description}
title={toolTitle}
description={toolDescription}
icon={icon}
type={type}
path={fullPath}
/>
{children}
<Separator backgroundColor="#5581b5" margin="50px" />
<AllTools
title={`All ${capitalizeFirstLetter(
getToolsByCategory().find((category) => category.type === type)!
.rawTitle
)} tools`}
title={t('translation:toolLayout.allToolsTitle', '', {
type: capitalizeFirstLetter(
getToolsByCategory(t).find((category) => category.type === type)!
.title
)
})}
toolCards={otherCategoryTools}
/>
</Box>

View File

@@ -1,10 +1,12 @@
import { Box, Grid, Stack, Typography } from '@mui/material';
import ToolCard from './ToolCard';
import { IconifyIcon } from '@iconify/react';
import { useTranslation } from 'react-i18next';
import { FullI18nKey } from '../../i18n';
export interface ToolCardProps {
title: string;
description: string;
title: FullI18nKey;
description: FullI18nKey;
link: string;
icon: IconifyIcon | string;
}
@@ -15,6 +17,7 @@ interface AllToolsProps {
}
export default function AllTools({ title, toolCards }: AllToolsProps) {
const { t } = useTranslation();
return (
<Box mt={4} mb={10}>
<Typography mb={2} fontSize={30} color={'primary'}>
@@ -25,8 +28,10 @@ export default function AllTools({ title, toolCards }: AllToolsProps) {
{toolCards.map((card, index) => (
<Grid item xs={12} md={6} lg={4} key={index}>
<ToolCard
title={card.title}
description={card.description}
//@ts-ignore
title={t(card.title)}
//@ts-ignore
description={t(card.description)}
link={card.link}
icon={card.icon}
/>

View File

@@ -3,6 +3,7 @@ import ExampleCard, { ExampleCardProps } from './ExampleCard';
import React from 'react';
import { GetGroupsType } from '@components/options/ToolOptions';
import { useFormikContext } from 'formik';
import { useTranslation } from 'react-i18next';
export type CardExampleType<T> = Omit<
ExampleCardProps<T>,
@@ -24,6 +25,7 @@ export default function ToolExamples<T>({
getGroups,
setInput
}: ExampleProps<T>) {
const { t } = useTranslation();
const { setValues } = useFormikContext<T>();
function changeInputResult(newInput: string | undefined, newOptions: T) {
@@ -39,10 +41,10 @@ export default function ToolExamples<T>({
<Box id={'examples'} mt={4}>
<Box mt={4} display="flex" gap={1} alignItems="center">
<Typography mb={2} fontSize={30} color={'primary'}>
{`${title} Examples`}
{t('toolExamples.title', { title })}
</Typography>
<Typography mb={2} fontSize={30} color={'secondary'}>
{subtitle ?? 'Click to try!'}
{subtitle ?? t('toolExamples.subtitle')}
</Typography>
</Box>

View File

@@ -12,6 +12,7 @@ import { globalInputHeight } from '../../config/uiConfig';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import greyPattern from '@assets/grey-pattern.png';
import { isArray } from 'lodash';
import { useTranslation } from 'react-i18next';
interface BaseFileInputComponentProps extends BaseFileInputProps {
children: (props: { preview: string | undefined }) => ReactNode;
@@ -26,6 +27,7 @@ export default function BaseFileInput({
children,
type
}: BaseFileInputComponentProps) {
const { t } = useTranslation();
const [preview, setPreview] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState<boolean>(false);
const theme = useTheme();
@@ -60,9 +62,9 @@ export default function BaseFileInput({
navigator.clipboard
.write([clipboardItem])
.then(() => showSnackBar('File copied', 'success'))
.then(() => showSnackBar(t('baseFileInput.fileCopied'), 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
showSnackBar(t('baseFileInput.copyFailed', { error: err }), 'error');
});
}
};
@@ -190,7 +192,7 @@ export default function BaseFileInput({
variant="h6"
align="center"
>
Drop your {type} here
{t('baseFileInput.dropFileHere', { type })}
</Typography>
) : (
<Typography
@@ -200,9 +202,7 @@ export default function BaseFileInput({
: theme.palette.grey['600']
}
>
Click here to select a {type} from your device, press Ctrl+V to
use a {type} from your clipboard, or drag and drop a file from
desktop
{t('baseFileInput.selectFileDescription', { type })}
</Typography>
)}
</Box>

View File

@@ -3,6 +3,7 @@ import Button from '@mui/material/Button';
import PublishIcon from '@mui/icons-material/Publish';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import ClearIcon from '@mui/icons-material/Clear';
import { useTranslation } from 'react-i18next';
export default function InputFooter({
handleImport,
@@ -13,19 +14,21 @@ export default function InputFooter({
handleCopy?: () => void;
handleClear?: () => void;
}) {
const { t } = useTranslation();
return (
<Stack mt={1} direction={'row'} spacing={2}>
<Button onClick={handleImport} startIcon={<PublishIcon />}>
Import from file
{t('inputFooter.importFromFile')}
</Button>
{handleCopy && (
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
Copy to clipboard
{t('inputFooter.copyToClipboard')}
</Button>
)}
{handleClear && (
<Button onClick={handleClear} startIcon={<ClearIcon />}>
Clear
{t('inputFooter.clear')}
</Button>
)}
</Stack>

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Grid, Select, MenuItem } from '@mui/material';
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
import Qty from 'js-quantities';
import { useTranslation } from 'react-i18next';
//
const siPrefixes: { [key: string]: number } = {
@@ -23,6 +24,7 @@ export default function NumericInputWithUnit(props: {
onOwnChange?: (value: { value: number; unit: string }) => void;
defaultPrefix?: string;
}) {
const { t } = useTranslation();
const [inputValue, setInputValue] = useState(props.value.value);
const [prefix, setPrefix] = useState(props.defaultPrefix || 'Default prefix');
@@ -158,7 +160,7 @@ export default function NumericInputWithUnit(props: {
<Select
fullWidth
disabled={disableChangingUnit}
placeholder={'Unit'}
placeholder={t('numericInputWithUnit.unit')}
sx={{ width: { xs: '75%', sm: '80%', md: '90%' } }}
value={unit}
onChange={(event) => {

View File

@@ -6,6 +6,7 @@ import InputFooter from './InputFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import { isArray } from 'lodash';
import MusicNoteIcon from '@mui/icons-material/MusicNote';
import { useTranslation } from 'react-i18next';
interface MultiAudioInputComponentProps {
accept: string[];
@@ -27,7 +28,10 @@ export default function ToolMultipleAudioInput({
title,
type
}: MultiAudioInputComponentProps) {
const { t } = useTranslation();
const theme = useTheme();
const fileInputRef = useRef<HTMLInputElement>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
@@ -93,7 +97,12 @@ export default function ToolMultipleAudioInput({
return (
<Box>
<InputHeader
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
title={
title ||
t('toolMultipleAudioInput.inputTitle', {
type: type.charAt(0).toUpperCase() + type.slice(1)
})
}
/>
<Box
sx={{
@@ -152,7 +161,7 @@ export default function ToolMultipleAudioInput({
))
) : (
<Typography variant="body2" color="text.secondary">
No files selected
{t('toolMultipleAudioInput.noFilesSelected')}
</Typography>
)}
</Box>

View File

@@ -6,6 +6,7 @@ import InputFooter from './InputFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import { isArray } from 'lodash';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import { useTranslation } from 'react-i18next';
interface MultiPdfInputComponentProps {
accept: string[];
@@ -27,6 +28,7 @@ export default function ToolMultiFileInput({
title,
type
}: MultiPdfInputComponentProps) {
const { t } = useTranslation();
const theme = useTheme();
const fileInputRef = useRef<HTMLInputElement>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
@@ -96,7 +98,12 @@ export default function ToolMultiFileInput({
return (
<Box>
<InputHeader
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
title={
title ||
t('toolMultiplePdfInput.inputTitle', {
type: type.charAt(0).toUpperCase() + type.slice(1)
})
}
/>
<Box
sx={{
@@ -156,7 +163,7 @@ export default function ToolMultiFileInput({
))
) : (
<Typography variant="body2" color="text.secondary">
No files selected
{t('toolMultiplePdfInput.noFilesSelected')}
</Typography>
)}
</Box>

View File

@@ -3,6 +3,7 @@ import React, { useContext, useRef } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
import InputFooter from './InputFooter';
import { useTranslation } from 'react-i18next';
export default function ToolTextInput({
value,
@@ -15,15 +16,16 @@ export default function ToolTextInput({
onChange: (value: string) => void;
placeholder?: string;
}) {
const { t } = useTranslation();
const { showSnackBar } = useContext(CustomSnackBarContext);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleCopy = () => {
navigator.clipboard
.writeText(value)
.then(() => showSnackBar('Text copied', 'success'))
.then(() => showSnackBar(t('toolTextInput.copied'), 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
showSnackBar(t('toolTextInput.copyFailed', { error: err }), 'error');
});
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -45,14 +47,14 @@ export default function ToolTextInput({
};
return (
<Box>
<InputHeader title={title} />
<InputHeader title={title || t('toolTextInput.input')} />
<TextField
value={value}
onChange={(event) => onChange(event.target.value)}
fullWidth
multiline
rows={10}
placeholder={placeholder}
placeholder={placeholder || t('toolTextInput.placeholder')}
sx={{
'&.MuiTextField-root': {
backgroundColor: 'background.paper'

View File

@@ -4,6 +4,7 @@ import Typography from '@mui/material/Typography';
import React, { ReactNode } from 'react';
import { FormikProps, FormikValues, useFormikContext } from 'formik';
import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups';
import { useTranslation } from 'react-i18next';
export type UpdateField<T> = <Y extends keyof T>(field: Y, value: T[Y]) => void;
type NonEmptyArray<T> = [T, ...T[]];
@@ -20,6 +21,7 @@ export default function ToolOptions<T extends FormikValues>({
getGroups: GetGroupsType<T> | null;
vertical?: boolean;
}) {
const { t } = useTranslation();
const theme = useTheme();
const formikContext = useFormikContext<T>();
@@ -45,7 +47,7 @@ export default function ToolOptions<T extends FormikValues>({
>
<Stack direction={'row'} spacing={1} alignItems={'center'}>
<SettingsIcon />
<Typography fontSize={22}>Tool options</Typography>
<Typography fontSize={22}>{t('toolOptions.title')}</Typography>
</Stack>
<Box mt={2}>
<Stack direction={'row'} spacing={2}>

View File

@@ -3,13 +3,14 @@ import Button from '@mui/material/Button';
import DownloadIcon from '@mui/icons-material/Download';
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function ResultFooter({
handleDownload,
handleCopy,
disabled,
hideCopy,
downloadLabel = 'Download'
downloadLabel
}: {
handleDownload: () => void;
handleCopy?: () => void;
@@ -17,6 +18,7 @@ export default function ResultFooter({
hideCopy?: boolean;
downloadLabel?: string;
}) {
const { t } = useTranslation();
return (
<Stack mt={1} direction={'row'} spacing={2}>
<Button
@@ -24,7 +26,7 @@ export default function ResultFooter({
onClick={handleDownload}
startIcon={<DownloadIcon />}
>
{downloadLabel}
{downloadLabel || t('resultFooter.download')}
</Button>
{!hideCopy && (
<Button
@@ -32,7 +34,7 @@ export default function ResultFooter({
onClick={handleCopy}
startIcon={<ContentPasteIcon />}
>
Copy to clipboard
{t('resultFooter.copy')}
</Button>
)}
</Stack>

View File

@@ -5,6 +5,7 @@ import greyPattern from '@assets/grey-pattern.png';
import { globalInputHeight } from '../../config/uiConfig';
import ResultFooter from './ResultFooter';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import { useTranslation } from 'react-i18next';
export default function ToolFileResult({
title = 'Result',
@@ -19,6 +20,7 @@ export default function ToolFileResult({
loading?: boolean;
loadingText?: string;
}) {
const { t } = useTranslation();
const [preview, setPreview] = React.useState<string | null>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
const theme = useTheme();
@@ -41,9 +43,9 @@ export default function ToolFileResult({
navigator.clipboard
.write([clipboardItem])
.then(() => showSnackBar('File copied', 'success'))
.then(() => showSnackBar(t('toolFileResult.copied'), 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
showSnackBar(t('toolFileResult.copyFailed', { error: err }), 'error');
});
}
};
@@ -91,7 +93,7 @@ export default function ToolFileResult({
return (
<Box>
<InputHeader title={title} />
<InputHeader title={title || t('toolFileResult.result')} />
<Box
sx={{
width: '100%',
@@ -114,7 +116,7 @@ export default function ToolFileResult({
>
<CircularProgress />
<Typography variant="body2" sx={{ mt: 2 }}>
{loadingText}... This may take a moment.
{loadingText || t('toolFileResult.loading')}
</Typography>
</Box>
) : (

View File

@@ -9,8 +9,11 @@ import InputHeader from '../InputHeader';
import greyPattern from '@assets/grey-pattern.png';
import { globalInputHeight } from '../../config/uiConfig';
import ResultFooter from './ResultFooter';
import { useTranslation } from 'react-i18next';
import React, { useContext } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
export default function ToolFileResult({
export default function ToolMultiFileResult({
title = 'Result',
value,
zipFile,
@@ -23,7 +26,9 @@ export default function ToolFileResult({
loading?: boolean;
loadingText?: string;
}) {
const { t } = useTranslation();
const theme = useTheme();
const { showSnackBar } = useContext(CustomSnackBarContext);
const getFileType = (
file: File
@@ -46,9 +51,25 @@ export default function ToolFileResult({
URL.revokeObjectURL(url);
};
const handleCopy = () => {
if (zipFile) {
const blob = new Blob([zipFile], { type: zipFile.type });
const clipboardItem = new ClipboardItem({ [zipFile.type]: blob });
navigator.clipboard
.write([clipboardItem])
.then(() => showSnackBar(t('toolMultiFileResult.copied'), 'success'))
.catch((err) => {
showSnackBar(
t('toolMultiFileResult.copyFailed', { error: err }),
'error'
);
});
}
};
return (
<Box>
<InputHeader title={title} />
<InputHeader title={title || t('toolMultiFileResult.result')} />
<Box
sx={{
width: '100%',
@@ -77,7 +98,7 @@ export default function ToolFileResult({
>
<CircularProgress />
<Typography variant="body2" sx={{ mt: 2 }}>
{loadingText}... This may take a moment.
{loadingText || t('toolMultiFileResult.loading')}
</Typography>
</Box>
) : (

View File

@@ -6,6 +6,7 @@ import ResultFooter from './ResultFooter';
import { replaceSpecialCharacters } from '@utils/string';
import mime from 'mime';
import { globalInputHeight } from '../../config/uiConfig';
import { useTranslation } from 'react-i18next';
export default function ToolTextResult({
title = 'Result',
@@ -20,13 +21,14 @@ export default function ToolTextResult({
keepSpecialCharacters?: boolean;
loading?: boolean;
}) {
const { t } = useTranslation();
const { showSnackBar } = useContext(CustomSnackBarContext);
const handleCopy = () => {
navigator.clipboard
.writeText(value)
.then(() => showSnackBar('Text copied', 'success'))
.then(() => showSnackBar(t('toolTextResult.copied'), 'success'))
.catch((err) => {
showSnackBar('Failed to copy: ' + err, 'error');
showSnackBar(t('toolTextResult.copyFailed', { error: err }), 'error');
});
};
const handleDownload = () => {
@@ -48,7 +50,7 @@ export default function ToolTextResult({
};
return (
<Box>
<InputHeader title={title} />
<InputHeader title={title || t('toolTextResult.result')} />
{loading ? (
<Box
sx={{
@@ -61,7 +63,7 @@ export default function ToolTextResult({
>
<CircularProgress />
<Typography variant="body2" sx={{ mt: 2 }}>
Loading... This may take a moment.
{t('toolTextResult.loading')}
</Typography>
</Box>
) : (