Add sorts to search items

This commit is contained in:
Zoe Roux 2023-10-31 21:10:35 +01:00
parent 68a3af0b52
commit 377d85c7f1
9 changed files with 198 additions and 159 deletions

View File

@ -16,7 +16,6 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
@ -33,7 +32,7 @@ namespace Kyoo.Core.Api
/// An endpoint to search for every resources of kyoo. Searching for only a specific type of resource /// An endpoint to search for every resources of kyoo. Searching for only a specific type of resource
/// is available on the said endpoint. /// is available on the said endpoint.
/// </summary> /// </summary>
[Route("search/{query?}")] [Route("search")]
[ApiController] [ApiController]
[ResourceView] [ResourceView]
[ApiDefinition("Search", Group = ResourcesGroup)] [ApiDefinition("Search", Group = ResourcesGroup)]
@ -54,7 +53,7 @@ namespace Kyoo.Core.Api
/// <remarks> /// <remarks>
/// Search for collections /// Search for collections
/// </remarks> /// </remarks>
/// <param name="query">The query to search for.</param> /// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param> /// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param> /// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
@ -64,12 +63,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Collection), Kind.Read)] [Permission(nameof(Collection), Kind.Read)]
[ApiDefinition("Collections")] [ApiDefinition("Collections")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Collection>> SearchCollections(string? query, public async Task<SearchPage<Collection>> SearchCollections(
[FromQuery] string? q,
[FromQuery] Sort<Collection> sortBy, [FromQuery] Sort<Collection> sortBy,
[FromQuery] SearchPagination pagination, [FromQuery] SearchPagination pagination,
[FromQuery] Include<Collection> fields) [FromQuery] Include<Collection> fields)
{ {
return SearchPage(await _searchManager.SearchCollections(query, sortBy, pagination, fields)); return SearchPage(await _searchManager.SearchCollections(q, sortBy, pagination, fields));
} }
/// <summary> /// <summary>
@ -78,7 +78,7 @@ namespace Kyoo.Core.Api
/// <remarks> /// <remarks>
/// Search for shows /// Search for shows
/// </remarks> /// </remarks>
/// <param name="query">The query to search for.</param> /// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param> /// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param> /// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
@ -88,12 +88,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Show), Kind.Read)] [Permission(nameof(Show), Kind.Read)]
[ApiDefinition("Show")] [ApiDefinition("Show")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Show>> SearchShows(string? query, public async Task<SearchPage<Show>> SearchShows(
[FromQuery] string? q,
[FromQuery] Sort<Show> sortBy, [FromQuery] Sort<Show> sortBy,
[FromQuery] SearchPagination pagination, [FromQuery] SearchPagination pagination,
[FromQuery] Include<Show> fields) [FromQuery] Include<Show> fields)
{ {
return SearchPage(await _searchManager.SearchShows(query, sortBy, pagination, fields)); return SearchPage(await _searchManager.SearchShows(q, sortBy, pagination, fields));
} }
/// <summary> /// <summary>
@ -102,7 +103,7 @@ namespace Kyoo.Core.Api
/// <remarks> /// <remarks>
/// Search for movie /// Search for movie
/// </remarks> /// </remarks>
/// <param name="query">The query to search for.</param> /// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param> /// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param> /// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
@ -112,12 +113,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Movie), Kind.Read)] [Permission(nameof(Movie), Kind.Read)]
[ApiDefinition("Movie")] [ApiDefinition("Movie")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Movie>> SearchMovies(string? query, public async Task<SearchPage<Movie>> SearchMovies(
[FromQuery] string? q,
[FromQuery] Sort<Movie> sortBy, [FromQuery] Sort<Movie> sortBy,
[FromQuery] SearchPagination pagination, [FromQuery] SearchPagination pagination,
[FromQuery] Include<Movie> fields) [FromQuery] Include<Movie> fields)
{ {
return SearchPage(await _searchManager.SearchMovies(query, sortBy, pagination, fields)); return SearchPage(await _searchManager.SearchMovies(q, sortBy, pagination, fields));
} }
/// <summary> /// <summary>
@ -126,7 +128,7 @@ namespace Kyoo.Core.Api
/// <remarks> /// <remarks>
/// Search for items /// Search for items
/// </remarks> /// </remarks>
/// <param name="query">The query to search for.</param> /// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param> /// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param> /// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
@ -136,12 +138,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(LibraryItem), Kind.Read)] [Permission(nameof(LibraryItem), Kind.Read)]
[ApiDefinition("Item")] [ApiDefinition("Item")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<LibraryItem>> SearchItems(string? query, public async Task<SearchPage<LibraryItem>> SearchItems(
[FromQuery] string? q,
[FromQuery] Sort<LibraryItem> sortBy, [FromQuery] Sort<LibraryItem> sortBy,
[FromQuery] SearchPagination pagination, [FromQuery] SearchPagination pagination,
[FromQuery] Include<LibraryItem> fields) [FromQuery] Include<LibraryItem> fields)
{ {
return SearchPage(await _searchManager.SearchItems(query, sortBy, pagination, fields)); return SearchPage(await _searchManager.SearchItems(q, sortBy, pagination, fields));
} }
/// <summary> /// <summary>
@ -150,7 +153,7 @@ namespace Kyoo.Core.Api
/// <remarks> /// <remarks>
/// Search for episodes /// Search for episodes
/// </remarks> /// </remarks>
/// <param name="query">The query to search for.</param> /// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param> /// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param> /// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
@ -160,12 +163,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Episode), Kind.Read)] [Permission(nameof(Episode), Kind.Read)]
[ApiDefinition("Episodes")] [ApiDefinition("Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Episode>> SearchEpisodes(string? query, public async Task<SearchPage<Episode>> SearchEpisodes(
[FromQuery] string? q,
[FromQuery] Sort<Episode> sortBy, [FromQuery] Sort<Episode> sortBy,
[FromQuery] SearchPagination pagination, [FromQuery] SearchPagination pagination,
[FromQuery] Include<Episode> fields) [FromQuery] Include<Episode> fields)
{ {
return SearchPage(await _searchManager.SearchEpisodes(query, sortBy, pagination, fields)); return SearchPage(await _searchManager.SearchEpisodes(q, sortBy, pagination, fields));
} }
/// <summary> /// <summary>
@ -174,7 +178,7 @@ namespace Kyoo.Core.Api
/// <remarks> /// <remarks>
/// Search for studios /// Search for studios
/// </remarks> /// </remarks>
/// <param name="query">The query to search for.</param> /// <param name="q">The query to search for.</param>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param> /// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param> /// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
@ -184,12 +188,13 @@ namespace Kyoo.Core.Api
[Permission(nameof(Studio), Kind.Read)] [Permission(nameof(Studio), Kind.Read)]
[ApiDefinition("Studios")] [ApiDefinition("Studios")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<SearchPage<Studio>> SearchStudios(string? query, public async Task<SearchPage<Studio>> SearchStudios(
[FromQuery] string? q,
[FromQuery] Sort<Studio> sortBy, [FromQuery] Sort<Studio> sortBy,
[FromQuery] SearchPagination pagination, [FromQuery] SearchPagination pagination,
[FromQuery] Include<Studio> fields) [FromQuery] Include<Studio> fields)
{ {
return SearchPage(await _searchManager.SearchStudios(query, sortBy, pagination, fields)); return SearchPage(await _searchManager.SearchStudios(q, sortBy, pagination, fields));
} }
} }
} }

View File

@ -33,21 +33,11 @@ namespace Kyoo.Meiliseach
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
public MeilisearchModule(IConfiguration configuration) public static Dictionary<string, Settings> IndexSettings => new()
{ {
_configuration = configuration;
}
/// <summary>
/// Init meilisearch indexes.
/// </summary>
/// <param name="provider">The service list to retrieve the meilisearch client</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task Initialize(IServiceProvider provider)
{ {
MeilisearchClient client = provider.GetRequiredService<MeilisearchClient>(); "items",
new Settings()
await _CreateIndex(client, "items", true, new Settings()
{ {
SearchableAttributes = new[] SearchableAttributes = new[]
{ {
@ -56,7 +46,7 @@ namespace Kyoo.Meiliseach
CamelCase.ConvertName(nameof(LibraryItem.Aliases)), CamelCase.ConvertName(nameof(LibraryItem.Aliases)),
CamelCase.ConvertName(nameof(LibraryItem.Path)), CamelCase.ConvertName(nameof(LibraryItem.Path)),
CamelCase.ConvertName(nameof(LibraryItem.Tags)), CamelCase.ConvertName(nameof(LibraryItem.Tags)),
// Overview could be included as well but I think it would be better without. CamelCase.ConvertName(nameof(LibraryItem.Overview)),
}, },
FilterableAttributes = new[] FilterableAttributes = new[]
{ {
@ -78,9 +68,11 @@ namespace Kyoo.Meiliseach
}, },
// TODO: Add stopwords // TODO: Add stopwords
// TODO: Extend default ranking to add ratings. // TODO: Extend default ranking to add ratings.
}); }
},
await _CreateIndex(client, nameof(Episode), false, new Settings() {
nameof(Episode),
new Settings()
{ {
SearchableAttributes = new[] SearchableAttributes = new[]
{ {
@ -106,9 +98,11 @@ namespace Kyoo.Meiliseach
CamelCase.ConvertName(nameof(Episode.Id)), CamelCase.ConvertName(nameof(Episode.Id)),
}, },
// TODO: Add stopwords // TODO: Add stopwords
}); }
},
await _CreateIndex(client, nameof(Studio), false, new Settings() {
nameof(Studio),
new Settings()
{ {
SearchableAttributes = new[] SearchableAttributes = new[]
{ {
@ -122,14 +116,34 @@ namespace Kyoo.Meiliseach
CamelCase.ConvertName(nameof(Studio.Id)), CamelCase.ConvertName(nameof(Studio.Id)),
}, },
// TODO: Add stopwords // TODO: Add stopwords
}); }
},
};
public MeilisearchModule(IConfiguration configuration)
{
_configuration = configuration;
} }
private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind, Settings opts) /// <summary>
/// Init meilisearch indexes.
/// </summary>
/// <param name="provider">The service list to retrieve the meilisearch client</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task Initialize(IServiceProvider provider)
{
MeilisearchClient client = provider.GetRequiredService<MeilisearchClient>();
await _CreateIndex(client, "items", true);
await _CreateIndex(client, nameof(Episode), false);
await _CreateIndex(client, nameof(Studio), false);
}
private static async Task _CreateIndex(MeilisearchClient client, string index, bool hasKind)
{ {
TaskInfo task = await client.CreateIndexAsync(index, hasKind ? "ref" : CamelCase.ConvertName(nameof(IResource.Id))); TaskInfo task = await client.CreateIndexAsync(index, hasKind ? "ref" : CamelCase.ConvertName(nameof(IResource.Id)));
await client.WaitForTaskAsync(task.TaskUid); await client.WaitForTaskAsync(task.TaskUid);
await client.Index(index).UpdateSettingsAsync(opts); await client.Index(index).UpdateSettingsAsync(IndexSettings[index]);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -32,13 +32,15 @@ public class SearchManager : ISearchManager
private readonly MeilisearchClient _client; private readonly MeilisearchClient _client;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private static IEnumerable<string> _GetSortsBy<T>(Sort<T>? sort) private static IEnumerable<string> _GetSortsBy<T>(string index, Sort<T>? sort)
{ {
return sort switch return sort switch
{ {
Sort<T>.Default => Array.Empty<string>(), Sort<T>.Default => Array.Empty<string>(),
Sort<T>.By @sortBy => new[] { $"{sortBy.Key}:{(sortBy.Desendant ? "desc" : "asc")}" }, Sort<T>.By @sortBy => MeilisearchModule.IndexSettings[index].SortableAttributes.Contains(sortBy.Key, StringComparer.InvariantCultureIgnoreCase)
Sort<T>.Conglomerate(var list) => list.SelectMany(_GetSortsBy), ? new[] { $"{CamelCase.ConvertName(sortBy.Key)}:{(sortBy.Desendant ? "desc" : "asc")}" }
: throw new ValidationException($"Invalid sorting mode: {sortBy.Key}"),
Sort<T>.Conglomerate(var list) => list.SelectMany(x => _GetSortsBy(index, x)),
Sort<T>.Random => throw new ValidationException("Random sorting is not supported while searching."), Sort<T>.Random => throw new ValidationException("Random sorting is not supported while searching."),
_ => Array.Empty<string>(), _ => Array.Empty<string>(),
}; };
@ -106,7 +108,7 @@ public class SearchManager : ISearchManager
ISearchable<IdResource> res = await _client.Index(index).SearchAsync<IdResource>(query, new SearchQuery() ISearchable<IdResource> res = await _client.Index(index).SearchAsync<IdResource>(query, new SearchQuery()
{ {
Filter = where, Filter = where,
Sort = _GetSortsBy(sortBy), Sort = _GetSortsBy(index, sortBy),
Limit = pagination?.Limit ?? 50, Limit = pagination?.Limit ?? 50,
Offset = pagination?.Skip ?? 0, Offset = pagination?.Skip ?? 0,
}); });
@ -126,7 +128,7 @@ public class SearchManager : ISearchManager
// TODO: add filters and facets // TODO: add filters and facets
ISearchable<IdResource> res = await _client.Index("items").SearchAsync<IdResource>(query, new SearchQuery() ISearchable<IdResource> res = await _client.Index("items").SearchAsync<IdResource>(query, new SearchQuery()
{ {
Sort = _GetSortsBy(sortBy), Sort = _GetSortsBy("items", sortBy),
Limit = pagination?.Limit ?? 50, Limit = pagination?.Limit ?? 50,
Offset = pagination?.Skip ?? 0, Offset = pagination?.Skip ?? 0,
}); });

View File

@ -1,24 +0,0 @@
/*
* Kyoo - A portable and vast media library solution.
* Copyright (c) Kyoo.
*
* See AUTHORS.md and LICENSE file in the project root for full license information.
*
* Kyoo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Kyoo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { BrowsePage } from "@kyoo/ui";
import { withRoute } from "~/router";
export default withRoute(BrowsePage);

View File

@ -37,11 +37,11 @@ import ViewList from "@material-symbols/svg-400/rounded/view_list.svg";
import Sort from "@material-symbols/svg-400/rounded/sort.svg"; import Sort from "@material-symbols/svg-400/rounded/sort.svg";
import ArrowUpward from "@material-symbols/svg-400/rounded/arrow_upward.svg"; import ArrowUpward from "@material-symbols/svg-400/rounded/arrow_upward.svg";
import ArrowDownward from "@material-symbols/svg-400/rounded/arrow_downward.svg"; import ArrowDownward from "@material-symbols/svg-400/rounded/arrow_downward.svg";
import { Layout, SortBy, SortOrd } from "./types"; import { Layout, SearchSort, SortOrd } from "./types";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { View, PressableProps } from "react-native"; import { View, PressableProps } from "react-native";
const SortTrigger = forwardRef<View, PressableProps & { sortKey: SortBy }>(function SortTrigger( const SortTrigger = forwardRef<View, PressableProps & { sortKey: string }>(function SortTrigger(
{ sortKey, ...props }, { sortKey, ...props },
ref, ref,
) { ) {
@ -61,15 +61,17 @@ const SortTrigger = forwardRef<View, PressableProps & { sortKey: SortBy }>(funct
}); });
export const BrowseSettings = ({ export const BrowseSettings = ({
availableSorts,
sortKey, sortKey,
sortOrd, sortOrd,
setSort, setSort,
layout, layout,
setLayout, setLayout,
}: { }: {
sortKey: SortBy; availableSorts: string[];
sortKey: string;
sortOrd: SortOrd; sortOrd: SortOrd;
setSort: (sort: SortBy, ord: SortOrd) => void; setSort: (sort: string, ord: SortOrd) => void;
layout: Layout; layout: Layout;
setLayout: (layout: Layout) => void; setLayout: (layout: Layout) => void;
}) => { }) => {
@ -97,12 +99,18 @@ export const BrowseSettings = ({
)} )}
<View {...css({ flexDirection: "row" })}> <View {...css({ flexDirection: "row" })}>
<Menu Trigger={SortTrigger} sortKey={sortKey}> <Menu Trigger={SortTrigger} sortKey={sortKey}>
{Object.values(SortBy).map((x) => ( {availableSorts.map((x) => (
<Menu.Item <Menu.Item
key={x} key={x}
label={t(`browse.sortkey.${x}`)} label={t(`browse.sortkey.${x}`)}
selected={sortKey === x} selected={sortKey === x}
icon={sortOrd === SortOrd.Asc ? ArrowUpward : ArrowDownward} icon={
x !== SearchSort.Relevance
? sortOrd === SortOrd.Asc
? ArrowUpward
: ArrowDownward
: undefined
}
onSelect={() => onSelect={() =>
setSort(x, sortKey === x && sortOrd === SortOrd.Asc ? SortOrd.Desc : SortOrd.Asc) setSort(x, sortKey === x && sortOrd === SortOrd.Asc ? SortOrd.Desc : SortOrd.Asc)
} }

View File

@ -53,20 +53,16 @@ export const itemMap = (
}; };
}; };
const query = ( const query = (sortKey?: SortBy, sortOrd?: SortOrd): QueryIdentifier<LibraryItem> => ({
slug?: string,
sortKey?: SortBy,
sortOrd?: SortOrd,
): QueryIdentifier<LibraryItem> => ({
parser: LibraryItemP, parser: LibraryItemP,
path: slug ? ["library", slug, "items"] : ["items"], path: ["items"],
infinite: true, infinite: true,
params: { params: {
sortBy: sortKey ? `${sortKey}:${sortOrd ?? "asc"}` : "name:asc", sortBy: sortKey ? `${sortKey}:${sortOrd ?? "asc"}` : "name:asc",
}, },
}); });
export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => { export const BrowsePage: QueryPage = () => {
const [sort, setSort] = useParam("sortBy"); const [sort, setSort] = useParam("sortBy");
const sortKey = (sort?.split(":")[0] as SortBy) || SortBy.Name; const sortKey = (sort?.split(":")[0] as SortBy) || SortBy.Name;
const sortOrd = (sort?.split(":")[1] as SortOrd) || SortOrd.Asc; const sortOrd = (sort?.split(":")[1] as SortOrd) || SortOrd.Asc;
@ -76,11 +72,12 @@ export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
return ( return (
<InfiniteFetch <InfiniteFetch
query={query(slug, sortKey, sortOrd)} query={query(sortKey, sortOrd)}
placeholderCount={15} placeholderCount={15}
layout={LayoutComponent.layout} layout={LayoutComponent.layout}
Header={ Header={
<BrowseSettings <BrowseSettings
availableSorts={Object.values(SortBy)}
sortKey={sortKey} sortKey={sortKey}
sortOrd={sortOrd} sortOrd={sortOrd}
setSort={(key, ord) => { setSort={(key, ord) => {
@ -98,6 +95,6 @@ export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
BrowsePage.getLayout = DefaultLayout; BrowsePage.getLayout = DefaultLayout;
BrowsePage.getFetchUrls = ({ slug, sortBy }) => [ BrowsePage.getFetchUrls = ({ sortBy }) => [
query(slug, sortBy?.split("-")[0] as SortBy, sortBy?.split("-")[1] as SortOrd), query(sortBy?.split("-")[0] as SortBy, sortBy?.split("-")[1] as SortOrd),
]; ];

View File

@ -25,6 +25,12 @@ export enum SortBy {
AddedDate = "addedDate", AddedDate = "addedDate",
} }
export enum SearchSort {
Relevance = "relevance",
AirDate = "airDate",
AddedDate = "addedDate",
}
export enum SortOrd { export enum SortOrd {
Asc = "asc", Asc = "asc",
Desc = "desc", Desc = "desc",

View File

@ -19,38 +19,67 @@
*/ */
import { LibraryItem, LibraryItemP, QueryIdentifier, QueryPage } from "@kyoo/models"; import { LibraryItem, LibraryItemP, QueryIdentifier, QueryPage } from "@kyoo/models";
import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ItemGrid } from "../browse/grid"; import { createParam } from "solito";
import { itemMap } from "../browse/index";
import { EmptyView } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { InfiniteFetch } from "../fetch-infinite";
import { itemMap } from "../browse";
import { SearchSort, SortOrd, SortBy, Layout } from "../browse/types";
import { BrowseSettings } from "../browse/header";
import { ItemGrid } from "../browse/grid";
import { ItemList } from "../browse/list";
const query = (query: string): QueryIdentifier<LibraryItem> => ({ const { useParam } = createParam<{ sortBy?: string }>();
const query = (
query?: string,
sortKey?: SearchSort,
sortOrd?: SortOrd,
): QueryIdentifier<LibraryItem> => ({
parser: LibraryItemP, parser: LibraryItemP,
path: ["search", query, "items"], path: ["search", "items"],
infinite: true, infinite: true,
getNext: () => undefined, params: {
q: query,
sortBy:
sortKey && sortKey != SearchSort.Relevance ? `${sortKey}:${sortOrd ?? "asc"}` : undefined,
},
}); });
export const SearchPage: QueryPage<{ q?: string }> = ({ q }) => { export const SearchPage: QueryPage<{ q?: string }> = ({ q }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [sort, setSort] = useParam("sortBy");
const sortKey = (sort?.split(":")[0] as SearchSort) || SearchSort.Relevance;
const sortOrd = (sort?.split(":")[1] as SortOrd) || SortOrd.Asc;
const [layout, setLayout] = useState(Layout.Grid);
const LayoutComponent = layout === Layout.Grid ? ItemGrid : ItemList;
const empty = <EmptyView message={t("search.empty")} />;
if (!q) return empty;
return ( return (
<InfiniteFetch <InfiniteFetch
query={query(q)} query={query(q, sortKey, sortOrd)}
// TODO: Understand why it does not work.
incremental={true}
layout={ItemGrid.layout}
placeholderCount={15} placeholderCount={15}
empty={empty} layout={LayoutComponent.layout}
Header={
<BrowseSettings
availableSorts={Object.values(SearchSort)}
sortKey={sortKey}
sortOrd={sortOrd}
setSort={(key, ord) => {
setSort(`${key}:${ord}`);
}}
layout={layout}
setLayout={setLayout}
/>
}
> >
{(item) => <ItemGrid {...itemMap(item)} />} {(item) => <LayoutComponent {...itemMap(item)} />}
</InfiniteFetch> </InfiniteFetch>
); );
}; };
SearchPage.getLayout = DefaultLayout; SearchPage.getLayout = DefaultLayout;
SearchPage.getFetchUrls = ({ q }) => (q ? [query(q)] : []); SearchPage.getFetchUrls = ({ q, sortBy }) => [
query(q, sortBy?.split("-")[0] as SearchSort, sortBy?.split("-")[1] as SortOrd),
];

View File

@ -23,7 +23,9 @@
"sortby": "Sort by {{key}}", "sortby": "Sort by {{key}}",
"sortby-tt": "Sort by", "sortby-tt": "Sort by",
"sortkey": { "sortkey": {
"relevance": "Relevance",
"name": "Name", "name": "Name",
"airDate": "Air Date",
"startAir": "Start air", "startAir": "Start air",
"endAir": "End air", "endAir": "End air",
"addedDate": "Added date" "addedDate": "Added date"