mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge branch 'main' of https://github.com/iib0011/omni-tools into fork/bhavesh158/json-compare
# Conflicts: # .idea/workspace.xml
This commit is contained in:
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
177
src/components/input/ToolMultipleVideoInput.tsx
Normal file
177
src/components/input/ToolMultipleVideoInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user