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

This commit is contained in:
AshAnand34
2025-07-11 14:59:10 -07:00
88 changed files with 3236 additions and 207 deletions

View File

@@ -6,6 +6,7 @@ import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
import { Link, useNavigate } from 'react-router-dom';
import logo from 'assets/logo.png';
import logoWhite from 'assets/logo-white.png';
import {
Drawer,
List,
@@ -107,17 +108,22 @@ const Navbar: React.FC<NavbarProps> = ({
sx={{
background: 'transparent',
boxShadow: 'none',
color: 'text.primary'
color: 'text.primary',
pt: 2
}}
>
<Toolbar
sx={{
justifyContent: 'space-between',
alignItems: 'center'
alignItems: 'center',
mx: { md: '50px', lg: '150px' }
}}
>
<Link to="/">
<img src={logo} width={isMobile ? '80px' : '150px'} />
<img
src={theme.palette.mode === 'light' ? logo : logoWhite}
width={isMobile ? '120px' : '200px'}
/>
</Link>
{isMobile ? (
<>

View File

@@ -46,6 +46,9 @@ interface ToolContentProps<Options, Input> extends ToolComponentProps {
setFieldValue: (fieldName: string, value: any) => void
) => ReactNode;
initialValues: Options;
/**
* should return non-empty array or null
*/
getGroups: GetGroupsType<Options> | null;
compute: (optionsValues: Options, input: Input) => void;
toolInfo?: {

View File

@@ -6,6 +6,7 @@ import Grid from '@mui/material/Grid';
import { Icon, IconifyIcon } from '@iconify/react';
import { categoriesColors } from '../config/uiConfig';
import { getToolsByCategory } from '@tools/index';
import { useEffect, useState } from 'react';
const StyledButton = styled(Button)(({ theme }) => ({
backgroundColor: 'white',
@@ -23,11 +24,25 @@ interface ToolHeaderProps {
}
function ToolLinks() {
const theme = useTheme();
const [examplesVisible, setExamplesVisible] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
const element = document.getElementById('examples');
if (element && isVisible(element)) {
setExamplesVisible(true);
}
}, 500);
return () => clearTimeout(timeout);
}, []);
const scrollToElement = (id: string) => {
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
};
function isVisible(elm: HTMLElement | null) {
return !!elm;
}
return (
<Grid container spacing={2} mt={1}>
<Grid item md={12} lg={6}>
@@ -40,16 +55,18 @@ function ToolLinks() {
Use This Tool
</StyledButton>
</Grid>
<Grid item md={12} lg={6}>
<StyledButton
fullWidth
variant="outlined"
sx={{ backgroundColor: 'background.paper' }}
onClick={() => scrollToElement('examples')}
>
See Examples
</StyledButton>
</Grid>
{examplesVisible && (
<Grid item md={12} lg={6}>
<StyledButton
fullWidth
variant="outlined"
sx={{ backgroundColor: 'background.paper' }}
onClick={() => scrollToElement('examples')}
>
See Examples
</StyledButton>
</Grid>
)}
{/*<Grid item md={12} lg={4}>*/}
{/* <StyledButton fullWidth variant="outlined" href="#tour">*/}
{/* Learn How to Use*/}

View File

@@ -12,7 +12,7 @@ export default function ToolInputAndResult({
return (
<Grid id="tool" container spacing={2}>
{input && (
<Grid item xs={12} md={6}>
<Grid item xs={12} md={result ? 6 : 12}>
{input}
</Grid>
)}

View File

@@ -7,5 +7,5 @@ a:hover {
}
* {
font-family: Plus Jakarta Sans, sans-serif;
font-family: Quicksand,sans-serif!important;
}

View File

@@ -0,0 +1,46 @@
import React, { useRef } from 'react';
import { Box, Typography } from '@mui/material';
import BaseFileInput from './BaseFileInput';
import { BaseFileInputProps } from './file-input-utils';
interface AudioFileInputProps extends Omit<BaseFileInputProps, 'accept'> {
accept?: string[];
}
export default function ToolAudioInput({
accept = ['audio/*', '.mp3', '.wav', '.aac'],
...props
}: AudioFileInputProps) {
const audioRef = useRef<HTMLAudioElement>(null);
return (
<BaseFileInput {...props} type={'audio'} accept={accept}>
{({ preview }) => (
<Box
sx={{
position: 'relative',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
{preview ? (
<audio
ref={audioRef}
src={preview}
style={{ maxWidth: '100%' }}
controls
/>
) : (
<Typography variant="body2" color="textSecondary">
Drag & drop or import an audio file
</Typography>
)}
</Box>
)}
</BaseFileInput>
);
}

View File

@@ -0,0 +1,172 @@
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 MusicNoteIcon from '@mui/icons-material/MusicNote';
interface MultiAudioInputComponentProps {
accept: string[];
title?: string;
type: 'audio';
value: MultiAudioInput[];
onChange: (file: MultiAudioInput[]) => void;
}
export interface MultiAudioInput {
file: File;
order: number;
}
export default function ToolMultipleAudioInput({
value,
onChange,
accept,
title,
type
}: MultiAudioInputComponentProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files)
onChange([
...value,
...Array.from(files).map((file) => ({ file, order: value.length }))
]);
};
const handleImportClick = () => {
fileInputRef.current?.click();
};
function handleClear() {
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' }}>
<MusicNoteIcon />
<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>
);
}

View File

@@ -7,11 +7,13 @@ import InputFooter from './InputFooter';
export default function ToolTextInput({
value,
onChange,
title = 'Input text'
title = 'Input text',
placeholder
}: {
title?: string;
value: string;
onChange: (value: string) => void;
placeholder?: string;
}) {
const { showSnackBar } = useContext(CustomSnackBarContext);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -50,6 +52,7 @@ export default function ToolTextInput({
fullWidth
multiline
rows={10}
placeholder={placeholder}
sx={{
'&.MuiTextField-root': {
backgroundColor: 'background.paper'

View File

@@ -6,10 +6,10 @@ import { FormikProps, FormikValues, useFormikContext } from 'formik';
import ToolOptionGroups, { ToolOptionGroup } from './ToolOptionGroups';
export type UpdateField<T> = <Y extends keyof T>(field: Y, value: T[Y]) => void;
type NonEmptyArray<T> = [T, ...T[]];
export type GetGroupsType<T> = (
formikProps: FormikProps<T> & { updateField: UpdateField<T> }
) => ToolOptionGroup[];
) => NonEmptyArray<ToolOptionGroup>;
export default function ToolOptions<T extends FormikValues>({
children,
@@ -50,7 +50,7 @@ export default function ToolOptions<T extends FormikValues>({
<Box mt={2}>
<Stack direction={'row'} spacing={2}>
<ToolOptionGroups
groups={getGroups({ ...formikContext, updateField }) ?? []}
groups={getGroups({ ...formikContext, updateField }) ?? null}
vertical={vertical}
/>
{children}