Search Provider icons

This commit is contained in:
Sean Edmunds
2025-11-06 13:25:46 -06:00
parent d317796d91
commit ed804a0182
8 changed files with 269 additions and 40 deletions

View File

@@ -29,23 +29,12 @@ export function AppItem({
const { ref, width, height } = useElementSize(); const { ref, width, height } = useElementSize();
const allIcons: any = { ...fa6, ...vsc, ...si }; const allIcons: any = { ...fa6, ...vsc, ...si };
const Icon = allIcons[icon]; const Icon = allIcons[icon];
const { isMobile, theme } = useCommonHooks(); const { isMobile, filter } = useCommonHooks();
const [textDecoration, setTextDecoration] = useState<string>('none'); const [textDecoration, setTextDecoration] = useState<string>('none');
const [hover, setHover] = useState<boolean>(false); const [hover, setHover] = useState<boolean>(false);
const validUrl = const validUrl =
url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}`; url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}`;
const filter = () => {
if (selectedTheme && selectedTheme.appShadowsEnabled === 1) {
return 'drop-shadow(2px 2px color-mix(in srgb, var(--mantine-color-accent-0), transparent 30%))';
}
if (!selectedTheme && theme.appShadowsEnabled === 1) {
return 'drop-shadow(2px 2px color-mix(in srgb, var(--mantine-color-accent-0), transparent 30%))';
}
return 'none';
};
return ( return (
<Box <Box
component='a' component='a'
@@ -113,7 +102,7 @@ export function AppItem({
style={{ style={{
height: scale * 32, height: scale * 32,
width: scale * 32, width: scale * 32,
filter: filter(), filter: filter(selectedTheme),
}} }}
/> />
)} )}

View File

@@ -65,16 +65,27 @@ export function useCommonHooks() {
key: 'providerPrefix', key: 'providerPrefix',
defaultValue: '/g', defaultValue: '/g',
}); });
const [defaultProviderIcon, setDefaultProviderIcon] = useLocalStorage<string>({
key: 'providerIcon',
defaultValue: 'FaGoogle',
});
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [providerLabel, setProviderLabel] = useState(defaultProviderLabel); const [providerLabel, setProviderLabel] = useState(defaultProviderLabel);
const [providerURL, setProviderURL] = useState(defaultProviderURL); const [providerURL, setProviderURL] = useState(defaultProviderURL);
const [providerPrefix, setProviderPrefix] = useState(defaultProviderPrefix); const [providerPrefix, setProviderPrefix] = useState(defaultProviderPrefix);
const [providerIcon, setProviderIcon] = useState(defaultProviderIcon);
useEffect(() => { useEffect(() => {
setProviderLabel(defaultProviderLabel); setProviderLabel(defaultProviderLabel);
setProviderURL(defaultProviderURL); setProviderURL(defaultProviderURL);
setProviderPrefix(defaultProviderPrefix); setProviderPrefix(defaultProviderPrefix);
}, [defaultProviderLabel, defaultProviderURL, defaultProviderPrefix]); setProviderIcon(defaultProviderIcon);
}, [
defaultProviderLabel,
defaultProviderURL,
defaultProviderPrefix,
defaultProviderIcon,
]);
const [currentUser, setCurrentUser] = useLocalStorage<User>({ const [currentUser, setCurrentUser] = useLocalStorage<User>({
key: 'user', key: 'user',
@@ -141,6 +152,17 @@ export function useCommonHooks() {
mutateAsync({ method: 'DELETE', endpoint: endpoint, body: item }); mutateAsync({ method: 'DELETE', endpoint: endpoint, body: item });
}; };
const filter = (selectedTheme?: Theme) => {
if (selectedTheme && selectedTheme.appShadowsEnabled === 1) {
return 'drop-shadow(2px 2px color-mix(in srgb, var(--mantine-color-accent-0), transparent 30%))';
}
if (!selectedTheme && theme.appShadowsEnabled === 1) {
return 'drop-shadow(2px 2px color-mix(in srgb, var(--mantine-color-accent-0), transparent 30%))';
}
return 'none';
};
function fixDigits(k: number) { function fixDigits(k: number) {
if (k < 10) { if (k < 10) {
return `0${k}`; return `0${k}`;
@@ -195,6 +217,7 @@ export function useCommonHooks() {
theme, theme,
setTheme, setTheme,
urlPattern, urlPattern,
filter,
fixDigits, fixDigits,
reorder, reorder,
getObjectDiff, getObjectDiff,
@@ -230,12 +253,16 @@ export function useCommonHooks() {
setProviderURL, setProviderURL,
providerPrefix, providerPrefix,
setProviderPrefix, setProviderPrefix,
providerIcon,
setProviderIcon,
defaultProviderLabel, defaultProviderLabel,
setDefaultProviderLabel, setDefaultProviderLabel,
defaultProviderURL, defaultProviderURL,
setDefaultProviderURL, setDefaultProviderURL,
defaultProviderPrefix, defaultProviderPrefix,
setDefaultProviderPrefix, setDefaultProviderPrefix,
defaultProviderIcon,
setDefaultProviderIcon,
currentUser, currentUser,
setCurrentUser, setCurrentUser,
currentLocation, currentLocation,

View File

@@ -81,6 +81,8 @@ export default function Search({
setProviderURL, setProviderURL,
providerPrefix, providerPrefix,
setProviderPrefix, setProviderPrefix,
providerIcon,
setProviderIcon,
searchValue, searchValue,
setSearchValue, setSearchValue,
defaultProviderURL, defaultProviderURL,
@@ -89,10 +91,13 @@ export default function Search({
setDefaultProviderLabel, setDefaultProviderLabel,
defaultProviderPrefix, defaultProviderPrefix,
setDefaultProviderPrefix, setDefaultProviderPrefix,
defaultProviderIcon,
setDefaultProviderIcon,
} = useCommonHooks(); } = useCommonHooks();
const [placeholderValue, setPlaceholderValue] = useState<string>(''); const [placeholderValue, setPlaceholderValue] = useState<string>('');
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const [historyMultiplier, setHistoryMultiplier] = useState(1); const [historyMultiplier, setHistoryMultiplier] = useState(1);
const Icon = allIcons[providerIcon];
const handleResetSearch = () => { const handleResetSearch = () => {
setSearchValue(''); setSearchValue('');
@@ -259,6 +264,7 @@ export default function Search({
setProviderLabel('Google Search'); setProviderLabel('Google Search');
setProviderURL('https://www.google.com/search?q='); setProviderURL('https://www.google.com/search?q=');
setProviderPrefix('/g'); setProviderPrefix('/g');
setProviderIcon('FaGoogle');
} else if (item.target.value.startsWith('/')) { } else if (item.target.value.startsWith('/')) {
const provider = providers?.find( const provider = providers?.find(
(p: Provider) => p.prefix === item.target.value.split(' ')[0], (p: Provider) => p.prefix === item.target.value.split(' ')[0],
@@ -266,6 +272,7 @@ export default function Search({
setProviderLabel(provider?.name ?? 'Invalid Search'); setProviderLabel(provider?.name ?? 'Invalid Search');
setProviderURL(provider?.url ?? ''); setProviderURL(provider?.url ?? '');
setProviderPrefix(provider?.prefix ?? ''); setProviderPrefix(provider?.prefix ?? '');
setProviderIcon(provider?.icon ?? '');
} else if (item.target.value.startsWith('!')) { } else if (item.target.value.startsWith('!')) {
if (item.target.value === '!w' || item.target.value === '!weather') { if (item.target.value === '!w' || item.target.value === '!weather') {
setProviderLabel('Show weather'); setProviderLabel('Show weather');
@@ -283,6 +290,7 @@ export default function Search({
setProviderLabel(defaultProviderLabel); setProviderLabel(defaultProviderLabel);
setProviderURL(defaultProviderURL); setProviderURL(defaultProviderURL);
setProviderPrefix(defaultProviderPrefix); setProviderPrefix(defaultProviderPrefix);
setProviderIcon(defaultProviderIcon);
} }
}; };
@@ -333,7 +341,12 @@ export default function Search({
}; };
const renderItem: any = (item: any) => { const renderItem: any = (item: any) => {
const Icon = allIcons[item.option.icon]; const Icon =
allIcons[
(item.option.icon ?? item.option.prefix === '/g')
? 'FaGoogle'
: (providers.find((p: Provider) => p.prefix === item.option.prefix)?.icon ?? '')
];
return !searchValue.startsWith('!') ? ( return !searchValue.startsWith('!') ? (
item.option.value === 'load' ? ( item.option.value === 'load' ? (
<Group <Group
@@ -363,6 +376,7 @@ export default function Search({
handleSearchHistoryItem={handleSearchHistoryItem} handleSearchHistoryItem={handleSearchHistoryItem}
searchEndpoint={searchEndpoint} searchEndpoint={searchEndpoint}
setSearchValue={setSearchValue} setSearchValue={setSearchValue}
Icon={Icon}
/> />
) )
) : ( ) : (
@@ -470,6 +484,7 @@ export default function Search({
); );
setDefaultProviderPrefix(defaultProviderData[0]?.prefix ?? '/g'); setDefaultProviderPrefix(defaultProviderData[0]?.prefix ?? '/g');
setDefaultProviderLabel(defaultProviderData[0]?.name ?? 'Google Search'); setDefaultProviderLabel(defaultProviderData[0]?.name ?? 'Google Search');
setDefaultProviderIcon(defaultProviderData[0]?.icon ?? 'FaGoogle');
} }
}, [defaultProviderData, defaultProviderDataIsFetched]); }, [defaultProviderData, defaultProviderDataIsFetched]);
@@ -520,7 +535,18 @@ export default function Search({
spellCheck={true} spellCheck={true}
searchable searchable
autoFocus={!isMobile} autoFocus={!isMobile}
leftSection={providerLabel} leftSection={
providerIcon ? (
<Icon
style={{
height: 32,
width: 32,
}}
/>
) : (
providerLabel
)
}
rightSection={<div />} rightSection={<div />}
clearable clearable
onClear={handleResetSearch} onClear={handleResetSearch}

View File

@@ -9,6 +9,7 @@ function SearchSelectItem({
handleSearchHistoryItem, handleSearchHistoryItem,
searchEndpoint, searchEndpoint,
setSearchValue, setSearchValue,
Icon,
}: any) { }: any) {
const { isMobile, theme } = useCommonHooks(); const { isMobile, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
@@ -22,7 +23,6 @@ function SearchSelectItem({
const dateString = isMobile const dateString = isMobile
? `${month}/${day}/${year}` ? `${month}/${day}/${year}`
: `${month}/${day}/${year} ${hours}:${minutes}`; : `${month}/${day}/${year} ${hours}:${minutes}`;
return ( return (
<Group <Group
w={'100%'} w={'100%'}
@@ -39,24 +39,48 @@ function SearchSelectItem({
setShowFillButton(false); setShowFillButton(false);
}} }}
> >
<Group m={0} p={0} w={'160px'}> <Group
<Text m={0}
style={{ p={0}
backgroundColor: `${theme.primaryColor}33`, w={'120px'}
fontWeight: 600, justify='center'
borderRadius: '15px', style={{
padding: '.25rem', overflow: 'hidden',
}} whiteSpace: 'nowrap',
> }}
{item.option.provider} >
</Text> {Icon ? (
<Icon
style={{
height: 32,
width: 32,
backgroundColor: `${theme.primaryColor}33`,
borderRadius: '15px',
padding: '.25rem',
}}
/>
) : (
<Text
style={{
backgroundColor: `${theme.primaryColor}33`,
fontWeight: 600,
borderRadius: '15px',
padding: '.25rem',
marginRight: '.25rem',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
>
{item.option.provider}
</Text>
)}
</Group> </Group>
<Group <Group
m={0} m={0}
p={0} p={0}
gap={0} gap={0}
justify='space-between' justify='space-between'
w={'calc(100% - 160px)'} w={'calc(100% - 120px)'}
style={{ style={{
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
overflow: 'hidden', overflow: 'hidden',

View File

@@ -1,17 +1,29 @@
import { Box, Card, Group, Stack, Text, TextInput } from '@mantine/core'; import {
Box,
Card,
type ComboboxItem,
type ComboboxLikeRenderOptionInput,
Group,
Select,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { DeleteItemModal, ManagementPill } from 'components'; import { DeleteItemModal, ManagementPill } from 'components';
import { useCommonHooks, useMutationAPI } from 'hooks'; import { useCommonHooks, useMutationAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { cardClasses, textClasses, textInputClasses } from 'styles'; import { cardClasses, selectClasses, textClasses, textInputClasses } from 'styles';
import type { Provider } from 'types'; import type { Provider } from 'types';
function DebouncedSearchProvider({ function DebouncedSearchProvider({
searchProvider, searchProvider,
setIsOpen, setIsOpen,
allIcons,
}: { }: {
searchProvider: Provider; searchProvider: Provider;
setIsOpen: (isOpen: boolean) => void; setIsOpen: (isOpen: boolean) => void;
allIcons: any;
}) { }) {
const { handleDelete, isMobile, theme } = useCommonHooks(); const { handleDelete, isMobile, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
@@ -81,7 +93,7 @@ function DebouncedSearchProvider({
</Box> </Box>
<Stack w={'100%'} align='center'> <Stack w={'100%'} align='center'>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center' pos={'relative'}>
<TextInput <TextInput
label={'Name'} label={'Name'}
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
@@ -100,6 +112,7 @@ function DebouncedSearchProvider({
} }
: undefined : undefined
} }
w={'30%'}
/> />
<TextInput <TextInput
label={'Prefix'} label={'Prefix'}
@@ -119,11 +132,63 @@ function DebouncedSearchProvider({
} }
: undefined : undefined
} }
w={'30%'}
/>
<Select
label={'Icon'}
className={selectClasses.settingsItem}
classNames={{
wrapper: selectClasses.menuSelectWrapper,
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
dropdown: selectClasses.dropdown,
}}
w={'30%'}
mt={'.75rem'}
limit={100}
placeholder='Search for an icon...'
defaultSearchValue={provider?.icon ?? ''}
defaultValue={provider?.icon}
data={Object.keys(allIcons).map((icon) => {
return { value: icon, label: icon };
})}
comboboxProps={{
zIndex: 3000,
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
onChange={(value: string | null) => {
setUpdate({
...provider,
icon: value,
});
}}
renderOption={(item: ComboboxLikeRenderOptionInput<ComboboxItem>) => {
const Icon = allIcons[item.option.value];
return (
<Group>
{item.option.value}
<Icon />
</Group>
);
}}
searchable
clearable
maxDropdownHeight={175}
onDropdownOpen={() => setIsOpen(true)}
onDropdownClose={() => setIsOpen(false)}
/> />
</Group> </Group>
<TextInput <TextInput
label={'Url'} label={'Url'}
w={isMobile ? '80%' : '100%'} w={isMobile ? '80%' : '94%'}
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
classNames={textInputClasses} classNames={textInputClasses}
defaultValue={provider.url} defaultValue={provider.url}

View File

@@ -1,11 +1,42 @@
import { Card, isLightColor, Stack, Text } from '@mantine/core'; import { Box, Card, isLightColor, Stack, Text } from '@mantine/core';
import HintItem from 'components/HelpModal/HintItem'; import HintItem from 'components/HelpModal/HintItem';
import { cardClasses } from 'styles'; import { cardClasses } from 'styles';
import type { Provider } from 'types'; import type { Provider } from 'types';
function SearchProviderSettingsItem({ provider }: { provider: Provider }) { function SearchProviderSettingsItem({
provider,
Icon,
filter,
}: {
provider: Provider;
Icon: any;
filter: any;
}) {
return ( return (
<Card className={cardClasses.settingsItemHover} classNames={cardClasses}> <Card className={cardClasses.settingsItemHover} classNames={cardClasses}>
{Icon && (
<Box
pos={'absolute'}
top={0}
left={0}
style={{
color: 'var(--mantine-color-primary-0)',
backgroundColor: 'var(--mantine-color-accent-0',
padding: '0.5rem',
borderBottomRightRadius: '.5rem',
}}
>
<Icon
style={{
height: 32,
width: 32,
filter: filter(),
padding: 0,
margin: 0,
}}
/>
</Box>
)}
<Stack <Stack
gap={0} gap={0}
style={{ style={{

View File

@@ -1,10 +1,13 @@
import { import {
Box, Box,
Button, Button,
type ComboboxItem,
type ComboboxLikeRenderOptionInput,
Grid, Grid,
GridCol, GridCol,
Group, Group,
Popover, Popover,
Select,
Stack, Stack,
Switch, Switch,
TextInput, TextInput,
@@ -15,13 +18,23 @@ import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { AddItemModal, NoItemsFound, SettingsHeader } from 'components'; import { AddItemModal, NoItemsFound, SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import * as fa6 from 'react-icons/fa6';
import { FiPower } from 'react-icons/fi'; import { FiPower } from 'react-icons/fi';
import { popoverClasses, switchClasses, textInputClasses, toolTipClasses } from 'styles'; import * as si from 'react-icons/si';
import * as vsc from 'react-icons/vsc';
import {
popoverClasses,
selectClasses,
switchClasses,
textInputClasses,
toolTipClasses,
} from 'styles';
import type { Provider } from 'types'; import type { Provider } from 'types';
import DebouncedSearchProvider from './DebouncedSearchProvider'; import DebouncedSearchProvider from './DebouncedSearchProvider';
import SearchProviderSettingsItem from './SearchProviderSettingsItem'; import SearchProviderSettingsItem from './SearchProviderSettingsItem';
const SearchProviderSettings = () => { const SearchProviderSettings = () => {
const allIcons: any = { ...fa6, ...vsc, ...si };
const defaultProvider = { const defaultProvider = {
id: undefined, id: undefined,
name: '', name: '',
@@ -42,7 +55,7 @@ const SearchProviderSettings = () => {
endpoint: endpoint, endpoint: endpoint,
}); });
const [providers, setProviders] = useState<Provider[]>([]); const [providers, setProviders] = useState<Provider[]>([]);
const { isMobile, theme } = useCommonHooks(); const { isMobile, theme, filter } = useCommonHooks();
const { mutateAsync } = useMutationAPI(); const { mutateAsync } = useMutationAPI();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
@@ -91,7 +104,7 @@ const SearchProviderSettings = () => {
> >
<Stack gap={0} w={'100%'} align='center'> <Stack gap={0} w={'100%'} align='center'>
<Stack w={'100%'} align='center'> <Stack w={'100%'} align='center'>
<Group w={'100%'} justify='center'> <Group w={'100%'} justify='center' pos={'relative'}>
<TextInput <TextInput
key={form.key('name')} key={form.key('name')}
{...form.getInputProps('name')} {...form.getInputProps('name')}
@@ -107,6 +120,7 @@ const SearchProviderSettings = () => {
} }
: undefined : undefined
} }
w={'30%'}
/> />
<TextInput <TextInput
key={form.key('prefix')} key={form.key('prefix')}
@@ -123,6 +137,52 @@ const SearchProviderSettings = () => {
} }
: undefined : undefined
} }
w={'30%'}
/>
<Select
key={form.key('icon')}
{...form.getInputProps('icon')}
label={'Icon'}
className={selectClasses.settingsItem}
classNames={{
wrapper: selectClasses.menuSelectWrapper,
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
dropdown: selectClasses.dropdown,
}}
w={'30%'}
mt={'.75rem'}
limit={100}
placeholder='Search for an icon...'
data={Object.keys(allIcons).map((icon) => {
return { value: icon, label: icon };
})}
comboboxProps={{
zIndex: 3000,
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
renderOption={(item: ComboboxLikeRenderOptionInput<ComboboxItem>) => {
const Icon = allIcons[item.option.value];
return (
<Group>
{item.option.value}
<Icon />
</Group>
);
}}
searchable
clearable
maxDropdownHeight={175}
onDropdownOpen={() => setIsOpen(true)}
onDropdownClose={() => setIsOpen(false)}
/> />
</Group> </Group>
<TextInput <TextInput
@@ -131,7 +191,7 @@ const SearchProviderSettings = () => {
label={'Url'} label={'Url'}
placeholder='eg. https://www.google.com/search?q=' placeholder='eg. https://www.google.com/search?q='
className={textInputClasses.settingsItem} className={textInputClasses.settingsItem}
w={'64%'} w={'94%'}
classNames={textInputClasses} classNames={textInputClasses}
withAsterisk withAsterisk
styles={ styles={
@@ -228,13 +288,18 @@ const SearchProviderSettings = () => {
> >
<Popover.Target> <Popover.Target>
<Box> <Box>
<SearchProviderSettingsItem provider={provider} /> <SearchProviderSettingsItem
provider={provider}
Icon={allIcons[provider.icon || '']}
filter={filter}
/>
</Box> </Box>
</Popover.Target> </Popover.Target>
<Popover.Dropdown w={isMobile ? '90%' : undefined}> <Popover.Dropdown w={isMobile ? '90%' : undefined}>
<DebouncedSearchProvider <DebouncedSearchProvider
searchProvider={provider} searchProvider={provider}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
allIcons={allIcons}
/> />
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>

View File

@@ -4,4 +4,6 @@ export interface Provider {
url: string; url: string;
prefix: string; prefix: string;
default: number; default: number;
enabled: number;
icon: string | null;
} }