mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
✨ Implement new movie search modals
This commit is contained in:
@@ -20,83 +20,9 @@ interface IntegrationSelectorProps {
|
|||||||
export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
||||||
const { t } = useTranslation('layout/modals/add-app');
|
const { t } = useTranslation('layout/modals/add-app');
|
||||||
|
|
||||||
const data: SelectItem[] = [
|
const data = availableIntegrations.filter((x) =>
|
||||||
{
|
Object.keys(integrationFieldProperties).includes(x.value)
|
||||||
value: 'sabnzbd',
|
);
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/sabnzbd.png',
|
|
||||||
label: 'SABnzbd',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'nzbGet',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/nzbget.png',
|
|
||||||
label: 'NZBGet',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'deluge',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/deluge.png',
|
|
||||||
label: 'Deluge',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'transmission',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/transmission.png',
|
|
||||||
label: 'Transmission',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'qBittorrent',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/qbittorrent.png',
|
|
||||||
label: 'qBittorrent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'jellyseerr',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyseerr.png',
|
|
||||||
label: 'Jellyseerr',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'overseerr',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/overseerr.png',
|
|
||||||
label: 'Overseerr',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'sonarr',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/sonarr.png',
|
|
||||||
label: 'Sonarr',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'radarr',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/radarr.png',
|
|
||||||
label: 'Radarr',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'lidarr',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/lidarr.png',
|
|
||||||
label: 'Lidarr',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'readarr',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
|
|
||||||
label: 'Readarr',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'jellyfin',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
|
|
||||||
label: 'Jellyfin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'plex',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/plex.png',
|
|
||||||
label: 'Plex',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'pihole',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/pi-hole.png',
|
|
||||||
label: 'PiHole',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'adGuardHome',
|
|
||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/adguard-home.png',
|
|
||||||
label: 'AdGuard Home',
|
|
||||||
},
|
|
||||||
].filter((x) => Object.keys(integrationFieldProperties).includes(x.value));
|
|
||||||
|
|
||||||
const getNewProperties = (value: string | null): AppIntegrationPropertyType[] => {
|
const getNewProperties = (value: string | null): AppIntegrationPropertyType[] => {
|
||||||
if (!value) return [];
|
if (!value) return [];
|
||||||
@@ -181,3 +107,81 @@ const SelectItemComponent = forwardRef<HTMLDivElement, ItemProps>(
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const availableIntegrations = [
|
||||||
|
{
|
||||||
|
value: 'sabnzbd',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/sabnzbd.png',
|
||||||
|
label: 'SABnzbd',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'nzbGet',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/nzbget.png',
|
||||||
|
label: 'NZBGet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'deluge',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/deluge.png',
|
||||||
|
label: 'Deluge',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'transmission',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/transmission.png',
|
||||||
|
label: 'Transmission',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'qBittorrent',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/qbittorrent.png',
|
||||||
|
label: 'qBittorrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'jellyseerr',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyseerr.png',
|
||||||
|
label: 'Jellyseerr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'overseerr',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/overseerr.png',
|
||||||
|
label: 'Overseerr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sonarr',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/sonarr.png',
|
||||||
|
label: 'Sonarr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'radarr',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/radarr.png',
|
||||||
|
label: 'Radarr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'lidarr',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/lidarr.png',
|
||||||
|
label: 'Lidarr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'readarr',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/readarr.png',
|
||||||
|
label: 'Readarr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'jellyfin',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/jellyfin.png',
|
||||||
|
label: 'Jellyfin',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'plex',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/plex.png',
|
||||||
|
label: 'Plex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'pihole',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/pi-hole.png',
|
||||||
|
label: 'PiHole',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'adGuardHome',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/adguard-home.png',
|
||||||
|
label: 'AdGuard Home',
|
||||||
|
},
|
||||||
|
] as const satisfies Readonly<SelectItem[]>;
|
||||||
|
|||||||
@@ -301,12 +301,13 @@ const getOpenTarget = (config: ConfigType | undefined): '_blank' | '_self' => {
|
|||||||
return config.settings.common.searchEngine.properties.openInNewTab ? '_blank' : '_self';
|
return config.settings.common.searchEngine.properties.openInNewTab ? '_blank' : '_self';
|
||||||
};
|
};
|
||||||
|
|
||||||
const useOverseerrSearchQuery = (query: string, isEnabled: boolean) => {
|
export const useOverseerrSearchQuery = (query: string, isEnabled: boolean) => {
|
||||||
const { name: configName } = useConfigContext();
|
const { name: configName } = useConfigContext();
|
||||||
return api.overseerr.all.useQuery(
|
return api.overseerr.search.useQuery(
|
||||||
{
|
{
|
||||||
query,
|
query,
|
||||||
configName: configName!,
|
configName: configName!,
|
||||||
|
integration: 'overseerr',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: isEnabled,
|
enabled: isEnabled,
|
||||||
|
|||||||
204
src/components/layout/new-header/MovieModal.tsx
Normal file
204
src/components/layout/new-header/MovieModal.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
Loader,
|
||||||
|
Image as MantineImage,
|
||||||
|
Modal,
|
||||||
|
ScrollArea,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons-react';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { availableIntegrations } from '~/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/Components/InputElements/IntegrationSelector';
|
||||||
|
import { useConfigContext } from '~/config/provider';
|
||||||
|
import { RequestModal } from '~/modules/overseerr/RequestModal';
|
||||||
|
import { RouterOutputs, api } from '~/utils/api';
|
||||||
|
|
||||||
|
type MovieModalProps = {
|
||||||
|
opened: boolean;
|
||||||
|
closeModal: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryParamsSchema = z.object({
|
||||||
|
movie: z.literal('true'),
|
||||||
|
search: z.string().nonempty(),
|
||||||
|
type: z.enum(['jellyseerr', 'overseerr']),
|
||||||
|
});
|
||||||
|
|
||||||
|
type MovieResultsProps = z.infer<typeof queryParamsSchema> & {
|
||||||
|
closeModal: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MovieResults = ({ search, type, closeModal }: MovieResultsProps) => {
|
||||||
|
const { name: configName } = useConfigContext();
|
||||||
|
const { data: overseerrResults, isLoading } = api.overseerr.search.useQuery(
|
||||||
|
{
|
||||||
|
query: search,
|
||||||
|
configName: configName!,
|
||||||
|
integration: type,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchInterval: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Loader />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Text>
|
||||||
|
Found {overseerrResults?.length} results for <b>{search}</b>
|
||||||
|
</Text>
|
||||||
|
<Stack spacing="xs">
|
||||||
|
{overseerrResults?.map((result, index: number) => (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<MovieDisplay movie={result} type={type} closeModal={closeModal} />
|
||||||
|
{index < overseerrResults.length - 1 && <Divider variant="dashed" my="xs" />}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MovieModal = ({ opened, closeModal }: MovieModalProps) => {
|
||||||
|
const query = useRouter().query;
|
||||||
|
const queryParams = queryParamsSchema.safeParse(query);
|
||||||
|
|
||||||
|
if (!queryParams.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const integration = useMemo(() => {
|
||||||
|
return availableIntegrations.find((x) => x.value === queryParams.data.type)!;
|
||||||
|
}, [queryParams.data.type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={closeModal}
|
||||||
|
size="lg"
|
||||||
|
scrollAreaComponent={ScrollArea.Autosize}
|
||||||
|
title={
|
||||||
|
<Group>
|
||||||
|
<Image src={integration.image} width={30} height={30} alt={`${integration.label} icon`} />
|
||||||
|
<Title order={4}>{integration.label} movies</Title>
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MovieResults
|
||||||
|
movie={queryParams.data.movie}
|
||||||
|
search={queryParams.data.search}
|
||||||
|
type={queryParams.data.type}
|
||||||
|
closeModal={closeModal}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type MovieDisplayProps = {
|
||||||
|
movie: RouterOutputs['overseerr']['search'][number];
|
||||||
|
type: 'jellyseerr' | 'overseerr';
|
||||||
|
closeModal: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MovieDisplay = ({ movie, type, closeModal }: MovieDisplayProps) => {
|
||||||
|
const { t } = useTranslation('modules/common-media-cards');
|
||||||
|
const { config } = useConfigContext();
|
||||||
|
const [requestModalOpened, requestModal] = useDisclosure(false);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = config.apps.find((service) => service.integration.type === type);
|
||||||
|
|
||||||
|
const mediaUrl = movie.mediaInfo?.plexUrl ?? movie.mediaInfo?.mediaUrl;
|
||||||
|
const serviceUrl = service?.behaviour.externalUrl ? service.behaviour.externalUrl : service?.url;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group noWrap style={{ maxHeight: 250 }} p={0} m={0} spacing="xs" align="stretch">
|
||||||
|
<MantineImage
|
||||||
|
withPlaceholder
|
||||||
|
src={`https://image.tmdb.org/t/p/w600_and_h900_bestv2/${
|
||||||
|
movie.posterPath ?? movie.backdropPath
|
||||||
|
}`}
|
||||||
|
height={200}
|
||||||
|
width={150}
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<Stack justify="space-between">
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<Title lineClamp={2} order={5}>
|
||||||
|
{movie.title ?? movie.name ?? movie.originalName}
|
||||||
|
</Title>
|
||||||
|
<Text color="dimmed" size="xs" lineClamp={4}>
|
||||||
|
{movie.overview}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Group spacing="xs">
|
||||||
|
{!movie.mediaInfo?.mediaAddedAt && (
|
||||||
|
<>
|
||||||
|
<RequestModal
|
||||||
|
base={movie}
|
||||||
|
opened={requestModalOpened}
|
||||||
|
setOpened={requestModal.toggle}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
requestModal.open();
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
rightIcon={<IconDownload size={15} />}
|
||||||
|
>
|
||||||
|
{t('buttons.request')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{mediaUrl && (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
target="_blank"
|
||||||
|
variant="outline"
|
||||||
|
href={mediaUrl}
|
||||||
|
size="sm"
|
||||||
|
rightIcon={<IconPlayerPlay size={15} />}
|
||||||
|
>
|
||||||
|
{t('buttons.play')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{serviceUrl && (
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
target="_blank"
|
||||||
|
href={serviceUrl}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
rightIcon={<IconExternalLink size={15} />}
|
||||||
|
>
|
||||||
|
TMDb
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Autocomplete, Group, Kbd, Text, Tooltip, useMantineTheme } from '@mantine/core';
|
import { Autocomplete, Group, Kbd, Modal, Text, Tooltip, useMantineTheme } from '@mantine/core';
|
||||||
import { useHotkeys } from '@mantine/hooks';
|
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
||||||
import {
|
import {
|
||||||
IconBrandYoutube,
|
IconBrandYoutube,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
@@ -8,10 +8,13 @@ import {
|
|||||||
IconWorld,
|
IconWorld,
|
||||||
TablerIconsProps,
|
TablerIconsProps,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { ReactNode, forwardRef, useMemo, useRef, useState } from 'react';
|
import { ReactNode, forwardRef, useMemo, useRef, useState } from 'react';
|
||||||
import { useConfigContext } from '~/config/provider';
|
import { useConfigContext } from '~/config/provider';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
|
import { MovieModal } from './MovieModal';
|
||||||
|
|
||||||
export const Search = () => {
|
export const Search = () => {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
@@ -19,6 +22,8 @@ export const Search = () => {
|
|||||||
const { data: userWithSettings } = api.user.getWithSettings.useQuery();
|
const { data: userWithSettings } = api.user.getWithSettings.useQuery();
|
||||||
const { config } = useConfigContext();
|
const { config } = useConfigContext();
|
||||||
const { colors } = useMantineTheme();
|
const { colors } = useMantineTheme();
|
||||||
|
const router = useRouter();
|
||||||
|
const [showMovieModal, movieModal] = useDisclosure(router.query.movie === 'true');
|
||||||
|
|
||||||
const apps = useConfigApps(search);
|
const apps = useConfigApps(search);
|
||||||
const engines = generateEngines(
|
const engines = generateEngines(
|
||||||
@@ -31,47 +36,61 @@ export const Search = () => {
|
|||||||
const data = [...engines, ...apps];
|
const data = [...engines, ...apps];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<>
|
||||||
ref={ref}
|
<Autocomplete
|
||||||
radius="xl"
|
ref={ref}
|
||||||
w={400}
|
radius="xl"
|
||||||
variant="filled"
|
w={400}
|
||||||
placeholder="Search..."
|
variant="filled"
|
||||||
hoverOnSearchChange
|
placeholder="Search..."
|
||||||
autoFocus={typeof window !== 'undefined' && window.innerWidth > 768}
|
hoverOnSearchChange
|
||||||
rightSection={
|
autoFocus={typeof window !== 'undefined' && window.innerWidth > 768}
|
||||||
<IconSearch
|
rightSection={
|
||||||
onClick={() => ref.current?.focus()}
|
<IconSearch
|
||||||
color={colors.gray[5]}
|
onClick={() => ref.current?.focus()}
|
||||||
size={16}
|
color={colors.gray[5]}
|
||||||
stroke={1.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') {
|
|
||||||
// TODO: show movie modal
|
|
||||||
console.log('movie');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const target = userWithSettings?.settings.openSearchInNewTab ? '_blank' : '_self';
|
limit={8}
|
||||||
window.open(item.metaData.url, target);
|
value={search}
|
||||||
}}
|
onChange={setSearch}
|
||||||
aria-label="Search"
|
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') {
|
||||||
|
// TODO: show movie modal
|
||||||
|
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
|
||||||
|
opened={showMovieModal}
|
||||||
|
closeModal={() => {
|
||||||
|
movieModal.close();
|
||||||
|
router.push(router.pathname, undefined, { shallow: true });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { api } from '~/utils/api';
|
|||||||
|
|
||||||
import { useColorTheme } from '../../tools/color';
|
import { useColorTheme } from '../../tools/color';
|
||||||
import { MovieResult } from './Movie.d';
|
import { MovieResult } from './Movie.d';
|
||||||
import { Result } from './SearchResult.d';
|
import { Result } from './SearchResult';
|
||||||
import { TvShowResult, TvShowResultSeason } from './TvShow.d';
|
import { TvShowResult, TvShowResultSeason } from './TvShow.d';
|
||||||
|
|
||||||
interface RequestModalProps {
|
interface RequestModalProps {
|
||||||
@@ -70,7 +70,7 @@ export function MovieRequestModal({
|
|||||||
radius="lg"
|
radius="lg"
|
||||||
size="lg"
|
size="lg"
|
||||||
trapFocus
|
trapFocus
|
||||||
zIndex={150}
|
zIndex={250}
|
||||||
withinPortal
|
withinPortal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
title={
|
title={
|
||||||
@@ -155,6 +155,7 @@ export function TvRequestModal({
|
|||||||
onClose={() => setOpened(false)}
|
onClose={() => setOpened(false)}
|
||||||
radius="lg"
|
radius="lg"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
zIndex={250}
|
||||||
opened={opened}
|
opened={opened}
|
||||||
title={
|
title={
|
||||||
<Group>
|
<Group>
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ import axios from 'axios';
|
|||||||
import Consola from 'consola';
|
import Consola from 'consola';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { MovieResult } from '~/modules/overseerr/Movie';
|
import { MovieResult } from '~/modules/overseerr/Movie';
|
||||||
import { Result } from '~/modules/overseerr/SearchResult';
|
import { OriginalLanguage, Result } from '~/modules/overseerr/SearchResult';
|
||||||
import { TvShowResult } from '~/modules/overseerr/TvShow';
|
import { TvShowResult } from '~/modules/overseerr/TvShow';
|
||||||
import { getConfig } from '~/tools/config/getConfig';
|
import { getConfig } from '~/tools/config/getConfig';
|
||||||
|
|
||||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
export const overseerrRouter = createTRPCRouter({
|
export const overseerrRouter = createTRPCRouter({
|
||||||
all: publicProcedure
|
search: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
configName: z.string(),
|
configName: z.string(),
|
||||||
|
integration: z.enum(['overseerr', 'jellyseerr']),
|
||||||
query: z.string().or(z.undefined()),
|
query: z.string().or(z.undefined()),
|
||||||
limit: z.number().default(10),
|
limit: z.number().default(10),
|
||||||
})
|
})
|
||||||
@@ -21,9 +22,7 @@ export const overseerrRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
const config = getConfig(input.configName);
|
const config = getConfig(input.configName);
|
||||||
|
|
||||||
const app = config.apps.find(
|
const app = config.apps.find((app) => app.integration?.type === input.integration);
|
||||||
(app) => app.integration?.type === 'overseerr' || app.integration?.type === 'jellyseerr'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (input.query === '' || input.query === undefined) {
|
if (input.query === '' || input.query === undefined) {
|
||||||
return [];
|
return [];
|
||||||
@@ -44,7 +43,7 @@ export const overseerrRouter = createTRPCRouter({
|
|||||||
'X-Api-Key': apiKey,
|
'X-Api-Key': apiKey,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => res.data as Result[]);
|
.then((res) => res.data.results as Result[]);
|
||||||
|
|
||||||
return data.slice(0, input.limit);
|
return data.slice(0, input.limit);
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user