mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
@@ -15,11 +15,16 @@ import { useState } from 'react';
|
|||||||
import { DefinedTool } from '@tools/defineTool';
|
import { DefinedTool } from '@tools/defineTool';
|
||||||
import { filterTools, tools } from '@tools/index';
|
import { filterTools, tools } from '@tools/index';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import _ from 'lodash';
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { getToolCategoryTitle } from '@utils/string';
|
import { getToolCategoryTitle } from '@utils/string';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { validNamespaces } from '../i18n';
|
import { validNamespaces } from '../i18n';
|
||||||
|
import {
|
||||||
|
getBookmarkedToolPaths,
|
||||||
|
isBookmarked,
|
||||||
|
toggleBookmarked
|
||||||
|
} from '@utils/bookmark';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
|
||||||
const GroupHeader = styled('div')(({ theme }) => ({
|
const GroupHeader = styled('div')(({ theme }) => ({
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
@@ -36,61 +41,59 @@ const GroupItems = styled('ul')({
|
|||||||
padding: 0
|
padding: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type ToolInfo = {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
const { t } = useTranslation(validNamespaces);
|
const { t } = useTranslation(validNamespaces);
|
||||||
const [inputValue, setInputValue] = useState<string>('');
|
const [inputValue, setInputValue] = useState<string>('');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
|
const [filteredTools, setFilteredTools] = useState<DefinedTool[]>(tools);
|
||||||
|
const [bookmarkedToolPaths, setBookmarkedToolPaths] = useState<string[]>(
|
||||||
|
getBookmarkedToolPaths()
|
||||||
|
);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const exampleTools: { label: string; url: string; translationKey: string }[] =
|
const exampleTools: ToolInfo[] = [
|
||||||
[
|
{
|
||||||
{
|
label: t('translation:hero.examples.createTransparentImage'),
|
||||||
label: t('translation:hero.examples.createTransparentImage'),
|
url: '/image-generic/create-transparent'
|
||||||
url: '/image-generic/create-transparent',
|
},
|
||||||
translationKey: 'translation:hero.examples.createTransparentImage'
|
{
|
||||||
},
|
label: t('translation:hero.examples.prettifyJson'),
|
||||||
{
|
url: '/json/prettify'
|
||||||
label: t('translation:hero.examples.prettifyJson'),
|
},
|
||||||
url: '/json/prettify',
|
{
|
||||||
translationKey: 'translation:hero.examples.prettifyJson'
|
label: t('translation:hero.examples.changeGifSpeed'),
|
||||||
},
|
url: '/gif/change-speed'
|
||||||
{
|
},
|
||||||
label: t('translation:hero.examples.changeGifSpeed'),
|
{
|
||||||
url: '/gif/change-speed',
|
label: t('translation:hero.examples.sortList'),
|
||||||
translationKey: 'translation:hero.examples.changeGifSpeed'
|
url: '/list/sort'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('translation:hero.examples.sortList'),
|
label: t('translation:hero.examples.compressPng'),
|
||||||
url: '/list/sort',
|
url: '/png/compress-png'
|
||||||
translationKey: 'translation:hero.examples.sortList'
|
},
|
||||||
},
|
{
|
||||||
{
|
label: t('translation:hero.examples.splitText'),
|
||||||
label: t('translation:hero.examples.compressPng'),
|
url: '/string/split'
|
||||||
url: '/png/compress-png',
|
},
|
||||||
translationKey: 'translation:hero.examples.compressPng'
|
{
|
||||||
},
|
label: t('translation:hero.examples.splitPdf'),
|
||||||
{
|
url: '/pdf/split-pdf'
|
||||||
label: t('translation:hero.examples.splitText'),
|
},
|
||||||
url: '/string/split',
|
{
|
||||||
translationKey: 'translation:hero.examples.splitText'
|
label: t('translation:hero.examples.trimVideo'),
|
||||||
},
|
url: '/video/trim'
|
||||||
{
|
},
|
||||||
label: t('translation:hero.examples.splitPdf'),
|
{
|
||||||
url: '/pdf/split-pdf',
|
label: t('translation:hero.examples.calculateNumberSum'),
|
||||||
translationKey: 'translation:hero.examples.splitPdf'
|
url: '/number/sum'
|
||||||
},
|
}
|
||||||
{
|
];
|
||||||
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 handleInputChange = (
|
const handleInputChange = (
|
||||||
event: React.ChangeEvent<{}>,
|
event: React.ChangeEvent<{}>,
|
||||||
@@ -99,6 +102,24 @@ export default function Hero() {
|
|||||||
setInputValue(newInputValue);
|
setInputValue(newInputValue);
|
||||||
setFilteredTools(filterTools(tools, newInputValue, t));
|
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, label: t(tool.label) }];
|
||||||
|
})
|
||||||
|
: exampleTools;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box width={{ xs: '90%', md: '80%', lg: '60%' }}>
|
<Box width={{ xs: '90%', md: '80%', lg: '60%' }}>
|
||||||
@@ -159,14 +180,42 @@ export default function Hero() {
|
|||||||
{...props}
|
{...props}
|
||||||
onClick={() => navigate('/' + option.path)}
|
onClick={() => navigate('/' + option.path)}
|
||||||
>
|
>
|
||||||
<Stack direction={'row'} spacing={2} alignItems={'center'}>
|
<Stack
|
||||||
<Icon fontSize={20} icon={option.icon} />
|
direction={'row'}
|
||||||
<Box>
|
alignItems={'center'}
|
||||||
<Typography fontWeight={'bold'}>{t(option.name)}</Typography>
|
justifyContent={'space-between'}
|
||||||
<Typography fontSize={12}>
|
width={'100%'}
|
||||||
{t(option.shortDescription)}
|
>
|
||||||
</Typography>
|
<Stack direction={'row'} spacing={2} alignItems={'center'}>
|
||||||
</Box>
|
<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>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -177,7 +226,7 @@ export default function Hero() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Grid container spacing={2} mt={2}>
|
<Grid container spacing={2} mt={2}>
|
||||||
{exampleTools.map((tool) => (
|
{displayedTools.map((tool) => (
|
||||||
<Grid
|
<Grid
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
navigate(tool.url.startsWith('/') ? tool.url : `/${tool.url}`)
|
navigate(tool.url.startsWith('/') ? tool.url : `/${tool.url}`)
|
||||||
@@ -186,7 +235,7 @@ export default function Hero() {
|
|||||||
xs={12}
|
xs={12}
|
||||||
md={6}
|
md={6}
|
||||||
lg={4}
|
lg={4}
|
||||||
key={tool.translationKey}
|
key={tool.label}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -202,10 +251,30 @@ export default function Hero() {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: 'background.hover'
|
backgroundColor: 'background.hover'
|
||||||
}
|
},
|
||||||
|
height: '100%'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography>{tool.label}</Typography>
|
<Stack direction={'row'} spacing={1} alignItems={'center'}>
|
||||||
|
<Typography textAlign={'center'}>{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>
|
</Box>
|
||||||
</Grid>
|
</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 Typography from '@mui/material/Typography';
|
||||||
import ToolBreadcrumb from './ToolBreadcrumb';
|
import ToolBreadcrumb from './ToolBreadcrumb';
|
||||||
import { capitalizeFirstLetter } from '../utils/string';
|
import { capitalizeFirstLetter } from '../utils/string';
|
||||||
@@ -7,6 +7,8 @@ import { Icon, IconifyIcon } from '@iconify/react';
|
|||||||
import { categoriesColors } from '../config/uiConfig';
|
import { categoriesColors } from '../config/uiConfig';
|
||||||
import { getToolsByCategory } from '@tools/index';
|
import { getToolsByCategory } from '@tools/index';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { isBookmarked, toggleBookmarked } from '@utils/bookmark';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const StyledButton = styled(Button)(({ theme }) => ({
|
const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
@@ -22,6 +24,7 @@ interface ToolHeaderProps {
|
|||||||
description: string;
|
description: string;
|
||||||
icon?: IconifyIcon | string;
|
icon?: IconifyIcon | string;
|
||||||
type: string;
|
type: string;
|
||||||
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ToolLinks() {
|
function ToolLinks() {
|
||||||
@@ -82,8 +85,11 @@ export default function ToolHeader({
|
|||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
type
|
type,
|
||||||
|
path
|
||||||
}: ToolHeaderProps) {
|
}: ToolHeaderProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [bookmarked, setBookmarked] = useState<boolean>(isBookmarked(path));
|
||||||
return (
|
return (
|
||||||
<Box my={4}>
|
<Box my={4}>
|
||||||
<ToolBreadcrumb
|
<ToolBreadcrumb
|
||||||
@@ -100,9 +106,27 @@ export default function ToolHeader({
|
|||||||
/>
|
/>
|
||||||
<Grid mt={1} container spacing={2}>
|
<Grid mt={1} container spacing={2}>
|
||||||
<Grid item xs={12} md={8}>
|
<Grid item xs={12} md={8}>
|
||||||
<Typography mb={2} fontSize={30} color={'primary'}>
|
<Stack direction={'row'} spacing={2} alignItems={'center'}>
|
||||||
{title}
|
<Typography mb={2} fontSize={30} color={'primary'}>
|
||||||
</Typography>
|
{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>
|
<Typography fontSize={20}>{description}</Typography>
|
||||||
<ToolLinks />
|
<ToolLinks />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ import { FullI18nKey } from '../i18n';
|
|||||||
export default function ToolLayout({
|
export default function ToolLayout({
|
||||||
children,
|
children,
|
||||||
icon,
|
icon,
|
||||||
|
i18n,
|
||||||
type,
|
type,
|
||||||
i18n
|
fullPath
|
||||||
}: {
|
}: {
|
||||||
icon?: IconifyIcon | string;
|
icon?: IconifyIcon | string;
|
||||||
type: ToolCategory;
|
type: ToolCategory;
|
||||||
|
fullPath: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
i18n?: {
|
i18n?: {
|
||||||
name: FullI18nKey;
|
name: FullI18nKey;
|
||||||
@@ -68,6 +70,7 @@ export default function ToolLayout({
|
|||||||
description={toolDescription}
|
description={toolDescription}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
type={type}
|
type={type}
|
||||||
|
path={fullPath}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
<Separator backgroundColor="#5581b5" margin="50px" />
|
<Separator backgroundColor="#5581b5" margin="50px" />
|
||||||
|
|||||||
@@ -65,7 +65,12 @@ export const defineTool = (
|
|||||||
component: function ToolComponent() {
|
component: function ToolComponent() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<ToolLayout icon={icon} type={basePath} i18n={i18n}>
|
<ToolLayout
|
||||||
|
icon={icon}
|
||||||
|
type={basePath}
|
||||||
|
i18n={i18n}
|
||||||
|
fullPath={`${basePath}/${path}`}
|
||||||
|
>
|
||||||
<Component
|
<Component
|
||||||
title={t(i18n.name)}
|
title={t(i18n.name)}
|
||||||
longDescription={
|
longDescription={
|
||||||
|
|||||||
38
src/utils/bookmark.ts
Normal file
38
src/utils/bookmark.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const bookmarkedToolsKey = 'bookmarkedTools';
|
||||||
|
|
||||||
|
export function getBookmarkedToolPaths(): string[] {
|
||||||
|
return (
|
||||||
|
localStorage
|
||||||
|
.getItem(bookmarkedToolsKey)
|
||||||
|
?.split(',')
|
||||||
|
?.filter((path) => path) ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBookmarked(toolPath: string): boolean {
|
||||||
|
return getBookmarkedToolPaths().some((path) => path === toolPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleBookmarked(toolPath: string) {
|
||||||
|
if (isBookmarked(toolPath)) {
|
||||||
|
unbookmark(toolPath);
|
||||||
|
} else {
|
||||||
|
bookmark(toolPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bookmark(toolPath: string) {
|
||||||
|
localStorage.setItem(
|
||||||
|
bookmarkedToolsKey,
|
||||||
|
[toolPath, ...getBookmarkedToolPaths()].join(',')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbookmark(toolPath: string) {
|
||||||
|
localStorage.setItem(
|
||||||
|
bookmarkedToolsKey,
|
||||||
|
getBookmarkedToolPaths()
|
||||||
|
.filter((path) => path !== toolPath)
|
||||||
|
.join(',')
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user