From 3920263ecfbb90c42084a9dc65445f480135317f Mon Sep 17 00:00:00 2001 From: Anderson Shindy Oki Date: Wed, 16 Oct 2024 10:03:58 +0900 Subject: [PATCH 1/2] feat: Add search poster and categories --- bazarr/api/system/searches.py | 7 +- frontend/src/components/Search.test.tsx | 9 ++ frontend/src/components/Search.tsx | 132 +++++++++++++++++------- frontend/src/types/api.d.ts | 1 + 4 files changed, 109 insertions(+), 40 deletions(-) create mode 100644 frontend/src/components/Search.test.tsx diff --git a/bazarr/api/system/searches.py b/bazarr/api/system/searches.py index a5a3a4960..9bf10ec7a 100644 --- a/bazarr/api/system/searches.py +++ b/bazarr/api/system/searches.py @@ -3,7 +3,7 @@ from flask_restx import Resource, Namespace, reqparse from unidecode import unidecode -from app.config import settings +from app.config import base_url, settings from app.database import TableShows, TableMovies, database, select from ..utils import authenticate @@ -34,6 +34,7 @@ def get(self): search_list += database.execute( select(TableShows.title, TableShows.sonarrSeriesId, + TableShows.poster, TableShows.year) .order_by(TableShows.title)) \ .all() @@ -43,6 +44,7 @@ def get(self): search_list += database.execute( select(TableMovies.title, TableMovies.radarrId, + TableMovies.poster, TableMovies.year) .order_by(TableMovies.title)) \ .all() @@ -58,8 +60,11 @@ def get(self): if hasattr(x, 'sonarrSeriesId'): result['sonarrSeriesId'] = x.sonarrSeriesId + result['poster'] = f"{base_url}/images/series{x.poster}" if x.poster else None + else: result['radarrId'] = x.radarrId + result['poster'] = f"{base_url}/images/movies{x.poster}" if x.poster else None results.append(result) diff --git a/frontend/src/components/Search.test.tsx b/frontend/src/components/Search.test.tsx new file mode 100644 index 000000000..bac037c7f --- /dev/null +++ b/frontend/src/components/Search.test.tsx @@ -0,0 +1,9 @@ +import { describe, it } from "vitest"; +import { Search } from "@/components/index"; +import { render } from "@/tests"; + +describe("Search Bar", () => { + it.skip("should render the closed empty state", () => { + render(); + }); +}); diff --git a/frontend/src/components/Search.tsx b/frontend/src/components/Search.tsx index c0dde3bef..1235547e6 100644 --- a/frontend/src/components/Search.tsx +++ b/frontend/src/components/Search.tsx @@ -1,48 +1,51 @@ import { FunctionComponent, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Autocomplete, ComboboxItem, OptionsFilter, Text } from "@mantine/core"; +import { + ComboboxItem, + Image, + OptionsFilter, + Select, + Text, +} from "@mantine/core"; +import { ComboboxItemGroup } from "@mantine/core/lib/components/Combobox/Combobox.types"; import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { chain, includes } from "lodash"; import { useServerSearch } from "@/apis/hooks"; import { useDebouncedValue } from "@/utilities"; type SearchResultItem = { value: string; + label: string; link: string; + poster: string; + type: string; }; function useSearch(query: string) { const debouncedQuery = useDebouncedValue(query, 500); const { data } = useServerSearch(debouncedQuery, debouncedQuery.length >= 0); - const duplicates = chain(data) - .groupBy((item) => `${item.title} (${item.year})`) - .filter((group) => group.length > 1) - .map((group) => `${group[0].title} (${group[0].year})`) - .value(); - return useMemo( () => data?.map((v) => { - const { link, displayName } = (() => { - const hasDuplicate = includes(duplicates, `${v.title} (${v.year})`); - + const { link, label, poster, type, value } = (() => { if (v.sonarrSeriesId) { return { + poster: v.poster, link: `/series/${v.sonarrSeriesId}`, - displayName: hasDuplicate - ? `${v.title} (${v.year}) (S)` - : `${v.title} (${v.year})`, + type: "show", + label: `${v.title} (${v.year})`, + value: `s-${v.sonarrSeriesId}`, }; } if (v.radarrId) { return { + poster: v.poster, link: `/movies/${v.radarrId}`, - displayName: hasDuplicate - ? `${v.title} (${v.year}) (M)` - : `${v.title} (${v.year})`, + type: "movie", + value: `m-${v.radarrId}`, + label: `${v.title} (${v.year})`, }; } @@ -50,11 +53,14 @@ function useSearch(query: string) { })(); return { - value: displayName, - link, + value: value, + poster: poster, + label: label, + type: type, + link: link, }; }) ?? [], - [data, duplicates], + [data], ); } @@ -62,16 +68,25 @@ const optionsFilter: OptionsFilter = ({ options, search }) => { const lowercaseSearch = search.toLowerCase(); const trimmedSearch = search.trim(); - return (options as ComboboxItem[]).filter((option) => { - return ( - option.value.toLowerCase().includes(lowercaseSearch) || - option.value - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase() - .includes(trimmedSearch) - ); + (options as ComboboxItemGroup[]).map((c) => { + return { + group: c.group, + items: c.items.filter((option) => { + const o = option as ComboboxItem; + + return ( + o.label.toLowerCase().includes(lowercaseSearch) || + o.label + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .includes(trimmedSearch) + ); + }), + }; }); + + return options; }; const Search: FunctionComponent = () => { @@ -80,23 +95,62 @@ const Search: FunctionComponent = () => { const results = useSearch(query); + const groups = [ + { + group: "Series", + items: results + .filter((r) => r.type === "show") + .map((r) => { + return { + label: r.label, + value: r.value, + }; + }), + }, + { + group: "Movies", + items: results + .filter((r) => r.type === "movie") + .map((r) => { + return { + label: r.label, + value: r.value, + }; + }), + }, + ]; + return ( - } - renderOption={(input) => {input.option.value}} + { searchable scrollAreaProps={{ type: "auto" }} maxDropdownHeight={400} - data={groups} + data={results} value={query} onSearchChange={(a) => { setQuery(a);