Merge branch 'main' of https://github.com/iib0011/omni-tools into fork/bhavesh158/json-compare

# Conflicts:
#	.idea/workspace.xml
This commit is contained in:
Ibrahima G. Coulibaly
2025-07-18 03:28:14 +01:00
171 changed files with 13953 additions and 3412 deletions

View File

@@ -15,11 +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 { validNamespaces } from '../i18n';
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',
@@ -36,61 +41,59 @@ const GroupItems = styled('ul')({
padding: 0
});
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: { label: string; url: string; translationKey: string }[] =
[
{
label: t('translation:hero.examples.createTransparentImage'),
url: '/image-generic/create-transparent',
translationKey: 'translation:hero.examples.createTransparentImage'
},
{
label: t('translation:hero.examples.prettifyJson'),
url: '/json/prettify',
translationKey: 'translation:hero.examples.prettifyJson'
},
{
label: t('translation:hero.examples.changeGifSpeed'),
url: '/gif/change-speed',
translationKey: 'translation:hero.examples.changeGifSpeed'
},
{
label: t('translation:hero.examples.sortList'),
url: '/list/sort',
translationKey: 'translation:hero.examples.sortList'
},
{
label: t('translation:hero.examples.compressPng'),
url: '/png/compress-png',
translationKey: 'translation:hero.examples.compressPng'
},
{
label: t('translation:hero.examples.splitText'),
url: '/string/split',
translationKey: 'translation:hero.examples.splitText'
},
{
label: t('translation:hero.examples.splitPdf'),
url: '/pdf/split-pdf',
translationKey: 'translation:hero.examples.splitPdf'
},
{
label: t('translation:hero.examples.trimVideo'),
url: '/video/trim',
translationKey: 'translation:hero.examples.trimVideo'
},
{
label: t('translation:hero.examples.calculateNumberSum'),
url: '/number/sum',
translationKey: 'translation:hero.examples.calculateNumberSum'
}
];
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<{}>,
@@ -99,6 +102,24 @@ export default function Hero() {
setInputValue(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%' }}>
@@ -130,7 +151,7 @@ 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>
);
@@ -159,14 +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'}>{t(option.name)}</Typography>
<Typography fontSize={12}>
{t(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>
)}
@@ -177,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}`)
@@ -186,7 +235,7 @@ export default function Hero() {
xs={12}
md={6}
lg={4}
key={tool.translationKey}
key={tool.label}
>
<Box
sx={{
@@ -202,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

@@ -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,7 +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',
@@ -22,11 +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(() => {
@@ -47,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
@@ -82,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
@@ -100,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

@@ -17,11 +17,13 @@ import { FullI18nKey } from '../i18n';
export default function ToolLayout({
children,
icon,
i18n,
type,
i18n
fullPath
}: {
icon?: IconifyIcon | string;
type: ToolCategory;
fullPath: string;
children: ReactNode;
i18n?: {
name: FullI18nKey;
@@ -41,7 +43,7 @@ export default function ToolLayout({
const toolDescription: string = t(i18n.description);
const otherCategoryTools =
getToolsByCategory()
getToolsByCategory(t)
.find((category) => category.type === type)
?.tools.filter((tool) => t(tool.name) !== toolTitle)
.map((tool) => ({
@@ -68,14 +70,15 @@ export default function ToolLayout({
description={toolDescription}
icon={icon}
type={type}
path={fullPath}
/>
{children}
<Separator backgroundColor="#5581b5" margin="50px" />
<AllTools
title={t('toolLayout.allToolsTitle', {
title={t('translation:toolLayout.allToolsTitle', '', {
type: capitalizeFirstLetter(
getToolsByCategory().find((category) => category.type === type)!
.rawTitle
getToolsByCategory(t).find((category) => category.type === type)!
.title
)
})}
toolCards={otherCategoryTools}

View File

@@ -0,0 +1,177 @@
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 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,
title,
type
}: MultiVideoInputComponentProps) {
console.log('ToolMultipleVideoInput rendering with value:', value);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
console.log('File change event:', files);
if (files)
onChange([
...value,
...Array.from(files).map((file) => ({ file, order: value.length }))
]);
};
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 (
<Box>
<InputHeader
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
/>
<Box
sx={{
width: '100%',
height: '300px',
border: value?.length ? 0 : 1,
borderRadius: 2,
boxShadow: '5',
bgcolor: 'background.paper',
position: 'relative'
}}
>
<Box
width="100%"
height="100%"
sx={{
overflow: 'auto',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
position: 'relative'
}}
>
{value?.length ? (
value.map((file, index) => (
<Box
key={index}
sx={{
margin: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '200px',
border: 1,
borderRadius: 1,
padding: 1
}}
>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<VideoFileIcon />
<Typography sx={{ marginLeft: 1 }}>
{fileNameTruncate(file.file.name)}
</Typography>
</Box>
<Box
sx={{ cursor: 'pointer' }}
onClick={() => {
const updatedFiles = value.filter((_, i) => i !== index);
onChange(updatedFiles);
}}
>
</Box>
</Box>
))
) : (
<Typography variant="body2" color="text.secondary">
No files selected
</Typography>
)}
</Box>
</Box>
<InputFooter handleImport={handleImportClick} handleClear={handleClear} />
<input
ref={fileInputRef}
style={{ display: 'none' }}
type="file"
accept={accept.join(',')}
onChange={handleFileChange}
multiple={true}
/>
</Box>
);
}