import { Autocomplete, Group, Kbd, Modal, Text, Tooltip, useMantineTheme } from '@mantine/core'; import { useDisclosure, useHotkeys } from '@mantine/hooks'; import { IconBrandYoutube, IconDownload, IconMovie, IconSearch, IconWorld, TablerIconsProps, } from '@tabler/icons-react'; import { useRouter } from 'next/router'; import { ReactNode, forwardRef, useMemo, useRef, useState } from 'react'; import { useConfigContext } from '~/config/provider'; import { api } from '~/utils/api'; import { MovieModal } from './MovieModal'; type SearchProps = { isMobile?: boolean; }; export const Search = ({ isMobile }: SearchProps) => { const [search, setSearch] = useState(''); const ref = useRef(null); useHotkeys([['mod+K', () => ref.current?.focus()]]); const { data: userWithSettings } = api.user.withSettings.useQuery(); const { config } = useConfigContext(); const { colors } = useMantineTheme(); const router = useRouter(); const [showMovieModal, movieModal] = useDisclosure(router.query.movie === 'true'); const apps = useConfigApps(search); const engines = generateEngines( search, userWithSettings?.settings.searchTemplate ?? 'https://www.google.com/search?q=%s' ).filter( (engine) => engine.sort !== 'movie' || config?.apps.some((app) => app.integration.type === engine.value) ); const data = [...apps, ...engines]; return ( <> 768} rightSection={ ref.current?.focus()} color={colors.gray[5]} size={16} stroke={1.5} /> } limit={8} value={search} onChange={setSearch} data={data} itemComponent={SearchItemComponent} filter={(value, item: SearchAutoCompleteItem) => engines.some((engine) => engine.sort === item.sort) || item.value.toLowerCase().includes(value.trim().toLowerCase()) } classNames={{ input: 'dashboard-header-search-input', root: 'dashboard-header-search-root', }} onItemSubmit={(item: SearchAutoCompleteItem) => { setSearch(''); if (item.sort === 'movie') { const url = new URL(`${window.location.origin}${router.asPath}`); url.searchParams.set('movie', 'true'); url.searchParams.set('search', search); url.searchParams.set('type', item.value); router.push(url, undefined, { shallow: true }); movieModal.open(); return; } const target = userWithSettings?.settings.openSearchInNewTab ? '_blank' : '_self'; window.open(item.metaData.url, target); }} aria-label="Search" /> { movieModal.close(); const url = new URL(`${window.location.origin}${router.asPath}`); url.searchParams.delete('movie'); url.searchParams.delete('search'); url.searchParams.delete('type'); router.push(url, undefined, { shallow: true }); }} /> ); }; const SearchItemComponent = forwardRef( ({ icon, label, value, sort, ...others }, ref) => { let Icon = getItemComponent(icon); return ( {label} ); } ); const getItemComponent = (icon: SearchAutoCompleteItem['icon']) => { if (typeof icon !== 'string') { return icon; } return (props: TablerIconsProps) => ( ); }; const useConfigApps = (search: string) => { const { config } = useConfigContext(); return useMemo(() => { if (search.trim().length === 0) return []; const apps = config?.apps.filter((app) => app.name.toLowerCase().includes(search.toLowerCase()) ); return ( apps?.map((app) => ({ icon: app.appearance.iconUrl, label: app.name, value: app.name, sort: 'app', metaData: { url: app.behaviour.externalUrl, }, })) ?? [] ); }, [search, config]); }; type SearchAutoCompleteItem = { icon: ((props: TablerIconsProps) => ReactNode) | string; label: string; value: string; } & ( | { sort: 'web' | 'torrent' | 'youtube' | 'app'; metaData: { url: string; }; } | { sort: 'movie'; } ); const movieApps = ['overseerr', 'jellyseerr'] as const; const generateEngines = (searchValue: string, webTemplate: string) => searchValue.trim().length > 0 ? ([ { icon: IconWorld, label: `Search for ${searchValue} in the web`, value: `web`, sort: 'web', metaData: { url: webTemplate.includes('%s') ? webTemplate.replace('%s', searchValue) : webTemplate + searchValue, }, }, { icon: IconDownload, label: `Search for ${searchValue} torrents`, value: `torrent`, sort: 'torrent', metaData: { url: `https://www.torrentdownloads.me/search/?search=${searchValue}`, }, }, { icon: IconBrandYoutube, label: `Search for ${searchValue} on youtube`, value: 'youtube', sort: 'youtube', metaData: { url: `https://www.youtube.com/results?search_query=${searchValue}`, }, }, ...movieApps.map( (name) => ({ icon: IconMovie, label: `Search for ${searchValue} on ${name}`, value: name, sort: 'movie', }) as const ), ] as const satisfies Readonly) : [];