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 allIcons: any = { ...fa6, ...vsc, ...si };
const Icon = allIcons[icon];
const { isMobile, theme } = useCommonHooks();
const { isMobile, filter } = useCommonHooks();
const [textDecoration, setTextDecoration] = useState<string>('none');
const [hover, setHover] = useState<boolean>(false);
const validUrl =
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 (
<Box
component='a'
@@ -113,7 +102,7 @@ export function AppItem({
style={{
height: scale * 32,
width: scale * 32,
filter: filter(),
filter: filter(selectedTheme),
}}
/>
)}

View File

@@ -65,16 +65,27 @@ export function useCommonHooks() {
key: 'providerPrefix',
defaultValue: '/g',
});
const [defaultProviderIcon, setDefaultProviderIcon] = useLocalStorage<string>({
key: 'providerIcon',
defaultValue: 'FaGoogle',
});
const [searchValue, setSearchValue] = useState('');
const [providerLabel, setProviderLabel] = useState(defaultProviderLabel);
const [providerURL, setProviderURL] = useState(defaultProviderURL);
const [providerPrefix, setProviderPrefix] = useState(defaultProviderPrefix);
const [providerIcon, setProviderIcon] = useState(defaultProviderIcon);
useEffect(() => {
setProviderLabel(defaultProviderLabel);
setProviderURL(defaultProviderURL);
setProviderPrefix(defaultProviderPrefix);
}, [defaultProviderLabel, defaultProviderURL, defaultProviderPrefix]);
setProviderIcon(defaultProviderIcon);
}, [
defaultProviderLabel,
defaultProviderURL,
defaultProviderPrefix,
defaultProviderIcon,
]);
const [currentUser, setCurrentUser] = useLocalStorage<User>({
key: 'user',
@@ -141,6 +152,17 @@ export function useCommonHooks() {
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) {
if (k < 10) {
return `0${k}`;
@@ -195,6 +217,7 @@ export function useCommonHooks() {
theme,
setTheme,
urlPattern,
filter,
fixDigits,
reorder,
getObjectDiff,
@@ -230,12 +253,16 @@ export function useCommonHooks() {
setProviderURL,
providerPrefix,
setProviderPrefix,
providerIcon,
setProviderIcon,
defaultProviderLabel,
setDefaultProviderLabel,
defaultProviderURL,
setDefaultProviderURL,
defaultProviderPrefix,
setDefaultProviderPrefix,
defaultProviderIcon,
setDefaultProviderIcon,
currentUser,
setCurrentUser,
currentLocation,

View File

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

View File

@@ -9,6 +9,7 @@ function SearchSelectItem({
handleSearchHistoryItem,
searchEndpoint,
setSearchValue,
Icon,
}: any) {
const { isMobile, theme } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
@@ -22,7 +23,6 @@ function SearchSelectItem({
const dateString = isMobile
? `${month}/${day}/${year}`
: `${month}/${day}/${year} ${hours}:${minutes}`;
return (
<Group
w={'100%'}
@@ -39,24 +39,48 @@ function SearchSelectItem({
setShowFillButton(false);
}}
>
<Group m={0} p={0} w={'160px'}>
<Group
m={0}
p={0}
w={'120px'}
justify='center'
style={{
overflow: 'hidden',
whiteSpace: 'nowrap',
}}
>
{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
m={0}
p={0}
gap={0}
justify='space-between'
w={'calc(100% - 160px)'}
w={'calc(100% - 120px)'}
style={{
whiteSpace: 'nowrap',
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 { DeleteItemModal, ManagementPill } from 'components';
import { useCommonHooks, useMutationAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { cardClasses, textClasses, textInputClasses } from 'styles';
import { cardClasses, selectClasses, textClasses, textInputClasses } from 'styles';
import type { Provider } from 'types';
function DebouncedSearchProvider({
searchProvider,
setIsOpen,
allIcons,
}: {
searchProvider: Provider;
setIsOpen: (isOpen: boolean) => void;
allIcons: any;
}) {
const { handleDelete, isMobile, theme } = useCommonHooks();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
@@ -81,7 +93,7 @@ function DebouncedSearchProvider({
</Box>
<Stack w={'100%'} align='center'>
<Group w={'100%'} justify='center'>
<Group w={'100%'} justify='center' pos={'relative'}>
<TextInput
label={'Name'}
className={textInputClasses.settingsItem}
@@ -100,6 +112,7 @@ function DebouncedSearchProvider({
}
: undefined
}
w={'30%'}
/>
<TextInput
label={'Prefix'}
@@ -119,11 +132,63 @@ function DebouncedSearchProvider({
}
: 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>
<TextInput
label={'Url'}
w={isMobile ? '80%' : '100%'}
w={isMobile ? '80%' : '94%'}
className={textInputClasses.settingsItem}
classNames={textInputClasses}
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 { cardClasses } from 'styles';
import type { Provider } from 'types';
function SearchProviderSettingsItem({ provider }: { provider: Provider }) {
function SearchProviderSettingsItem({
provider,
Icon,
filter,
}: {
provider: Provider;
Icon: any;
filter: any;
}) {
return (
<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
gap={0}
style={{

View File

@@ -1,10 +1,13 @@
import {
Box,
Button,
type ComboboxItem,
type ComboboxLikeRenderOptionInput,
Grid,
GridCol,
Group,
Popover,
Select,
Stack,
Switch,
TextInput,
@@ -15,13 +18,23 @@ import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
import { AddItemModal, NoItemsFound, SettingsHeader } from 'components';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import * as fa6 from 'react-icons/fa6';
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 DebouncedSearchProvider from './DebouncedSearchProvider';
import SearchProviderSettingsItem from './SearchProviderSettingsItem';
const SearchProviderSettings = () => {
const allIcons: any = { ...fa6, ...vsc, ...si };
const defaultProvider = {
id: undefined,
name: '',
@@ -42,7 +55,7 @@ const SearchProviderSettings = () => {
endpoint: endpoint,
});
const [providers, setProviders] = useState<Provider[]>([]);
const { isMobile, theme } = useCommonHooks();
const { isMobile, theme, filter } = useCommonHooks();
const { mutateAsync } = useMutationAPI();
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const [searchValue, setSearchValue] = useState('');
@@ -91,7 +104,7 @@ const SearchProviderSettings = () => {
>
<Stack gap={0} w={'100%'} align='center'>
<Stack w={'100%'} align='center'>
<Group w={'100%'} justify='center'>
<Group w={'100%'} justify='center' pos={'relative'}>
<TextInput
key={form.key('name')}
{...form.getInputProps('name')}
@@ -107,6 +120,7 @@ const SearchProviderSettings = () => {
}
: undefined
}
w={'30%'}
/>
<TextInput
key={form.key('prefix')}
@@ -123,6 +137,52 @@ const SearchProviderSettings = () => {
}
: 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>
<TextInput
@@ -131,7 +191,7 @@ const SearchProviderSettings = () => {
label={'Url'}
placeholder='eg. https://www.google.com/search?q='
className={textInputClasses.settingsItem}
w={'64%'}
w={'94%'}
classNames={textInputClasses}
withAsterisk
styles={
@@ -228,13 +288,18 @@ const SearchProviderSettings = () => {
>
<Popover.Target>
<Box>
<SearchProviderSettingsItem provider={provider} />
<SearchProviderSettingsItem
provider={provider}
Icon={allIcons[provider.icon || '']}
filter={filter}
/>
</Box>
</Popover.Target>
<Popover.Dropdown w={isMobile ? '90%' : undefined}>
<DebouncedSearchProvider
searchProvider={provider}
setIsOpen={setIsOpen}
allIcons={allIcons}
/>
</Popover.Dropdown>
</Popover>

View File

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