mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Create a search page
This commit is contained in:
parent
8ca120aa6f
commit
942f4f1c75
@ -21,7 +21,7 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { ThemeSelector } from "@kyoo/primitives";
|
||||
import { useTheme } from "yoshiki/native";
|
||||
import { LoginAvatar, NavbarTitle } from "@kyoo/ui";
|
||||
import { NavbarRight, NavbarTitle } from "@kyoo/ui";
|
||||
import { useState } from "react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createQueryClient } from "@kyoo/models";
|
||||
@ -54,7 +54,7 @@ const ThemedStack = () => {
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerTitle: () => <NavbarTitle />,
|
||||
headerRight: () => <LoginAvatar />,
|
||||
headerRight: () => <NavbarRight />,
|
||||
headerStyle: {
|
||||
backgroundColor: theme.appbar,
|
||||
},
|
||||
|
24
front/apps/mobile/app/search/index.tsx
Normal file
24
front/apps/mobile/app/search/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { SearchPage } from "@kyoo/ui";
|
||||
import { withRoute } from "../../utils";
|
||||
|
||||
export default withRoute(SearchPage);
|
@ -20,7 +20,6 @@
|
||||
|
||||
import "../polyfill";
|
||||
|
||||
import { createTheme, ThemeProvider as MTheme } from "@mui/material";
|
||||
import { Hydrate, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ReactNode, useState } from "react";
|
||||
import NextApp, { AppContext, type AppProps } from "next/app";
|
||||
@ -92,7 +91,9 @@ const GlobalCssTheme = () => {
|
||||
const App = ({ Component, pageProps }: AppProps) => {
|
||||
const [queryClient] = useState(() => createQueryClient());
|
||||
const { queryState, ...props } = superjson.deserialize<any>(pageProps ?? { json: {} });
|
||||
const Layout = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
||||
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
||||
const { Layout, props: layoutProps } =
|
||||
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
||||
|
||||
useMobileHover();
|
||||
|
||||
@ -105,7 +106,7 @@ const App = ({ Component, pageProps }: AppProps) => {
|
||||
<Hydrate state={queryState}>
|
||||
<ThemeSelector>
|
||||
<GlobalCssTheme />
|
||||
<Layout page={<Component {...props} />} />
|
||||
<Layout page={<Component {...props} />} {...layoutProps} />
|
||||
</ThemeSelector>
|
||||
</Hydrate>
|
||||
</QueryClientProvider>
|
||||
|
24
front/apps/web/src/pages/search/index.tsx
Normal file
24
front/apps/web/src/pages/search/index.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 { SearchPage } from "@kyoo/ui";
|
||||
import { withRoute } from "~/router";
|
||||
|
||||
export default withRoute(SearchPage);
|
@ -18,12 +18,13 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ComponentType, ReactElement, ReactNode } from "react";
|
||||
import { ComponentProps, ComponentType, ReactElement } from "react";
|
||||
import {
|
||||
dehydrate,
|
||||
QueryClient,
|
||||
QueryFunctionContext,
|
||||
useInfiniteQuery,
|
||||
UseInfiniteQueryOptions,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { z } from "zod";
|
||||
@ -40,8 +41,8 @@ const queryFn = async <Data,>(
|
||||
? process.env.PUBLIC_BACK_URL
|
||||
: typeof window === "undefined"
|
||||
? process.env.KYOO_URL ?? "http://localhost:5000"
|
||||
// TODO remove the hardcoded fallback. This is just for testing purposes
|
||||
: "/api") ?? "https://beta.sdg.moe";
|
||||
: // TODO remove the hardcoded fallback. This is just for testing purposes
|
||||
"/api") ?? "https://beta.sdg.moe";
|
||||
if (!kyooUrl) console.error("Kyoo's url is not defined.");
|
||||
|
||||
let resp;
|
||||
@ -105,11 +106,17 @@ export type QueryIdentifier<T = unknown> = {
|
||||
path: (string | undefined)[];
|
||||
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
||||
infinite?: boolean;
|
||||
/**
|
||||
* A custom get next function if the infinite query is not a page.
|
||||
*/
|
||||
getNext?: (item: unknown) => string | undefined;
|
||||
};
|
||||
|
||||
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
||||
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
|
||||
getLayout?: ({ page }: { page: ReactElement }) => JSX.Element;
|
||||
getLayout?:
|
||||
| ComponentType<{ page: ReactElement }>
|
||||
| { Layout: ComponentType<{ page: ReactElement }>; props: object };
|
||||
};
|
||||
|
||||
const toQueryKey = <Data,>(query: QueryIdentifier<Data>) => {
|
||||
@ -134,7 +141,21 @@ export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useInfiniteFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
||||
export const useInfiniteFetch = <Data,>(
|
||||
query: QueryIdentifier<Data>,
|
||||
options?: Partial<UseInfiniteQueryOptions<Data[], KyooErrors>>,
|
||||
) => {
|
||||
if (query.getNext) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const ret = useInfiniteQuery<Data[], KyooErrors>({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) => queryFn(z.array(query.parser), ctx),
|
||||
getNextPageParam: query.getNext,
|
||||
...options,
|
||||
});
|
||||
return { ...ret, items: ret.data?.pages.flatMap((x) => x) };
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const ret = useInfiniteQuery<Page<Data>, KyooErrors>({
|
||||
queryKey: toQueryKey(query),
|
||||
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
|
||||
|
@ -32,6 +32,7 @@ export * from "./divider";
|
||||
export * from "./progress";
|
||||
export * from "./slider";
|
||||
export * from "./menu";
|
||||
export * from "./input";
|
||||
|
||||
export * from "./animated";
|
||||
export * from "./utils";
|
||||
|
56
front/packages/primitives/src/input.tsx
Normal file
56
front/packages/primitives/src/input.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 { TextInput } from "react-native";
|
||||
import { px, Stylable, useYoshiki } from "yoshiki/native";
|
||||
import { ts } from "./utils";
|
||||
|
||||
export const Input = ({
|
||||
onChange,
|
||||
value,
|
||||
placeholder,
|
||||
placeholderTextColor,
|
||||
...props
|
||||
}: {
|
||||
onChange: (value: string) => void;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
placeholderTextColor?: string;
|
||||
} & Stylable) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
value={value ?? ""}
|
||||
onChangeText={onChange}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={placeholderTextColor ?? theme.overlay1}
|
||||
{...css(
|
||||
{
|
||||
borderColor: (theme) => theme.accent,
|
||||
borderRadius: ts(1),
|
||||
borderWidth: px(1),
|
||||
padding: ts(0.5),
|
||||
},
|
||||
props,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -34,7 +34,7 @@ import { ItemGrid } from "./grid";
|
||||
import { ItemList } from "./list";
|
||||
import { SortBy, SortOrd, Layout } from "./types";
|
||||
|
||||
const itemMap = (
|
||||
export const itemMap = (
|
||||
item: WithLoading<LibraryItem>,
|
||||
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
|
||||
if (item.isLoading) return item;
|
||||
|
@ -21,7 +21,7 @@
|
||||
import { Movie, MovieP, QueryIdentifier, QueryPage } from "@kyoo/models";
|
||||
import { Platform, ScrollView } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { TransparentLayout } from "../layout";
|
||||
import { DefaultLayout } from "../layout";
|
||||
import { Header } from "./header";
|
||||
|
||||
const query = (slug: string): QueryIdentifier<Movie> => ({
|
||||
@ -48,4 +48,4 @@ MovieDetails.getFetchUrls = ({ slug }) => [
|
||||
// ShowStaff.query(slug),
|
||||
];
|
||||
|
||||
MovieDetails.getLayout = TransparentLayout;
|
||||
MovieDetails.getLayout = { Layout: DefaultLayout, props: { transparent: true } };
|
||||
|
@ -19,9 +19,9 @@
|
||||
*/
|
||||
|
||||
import { QueryIdentifier, QueryPage, Show, ShowP } from "@kyoo/models";
|
||||
import { Platform, ScrollView, View, ViewProps } from "react-native";
|
||||
import { Platform, View, ViewProps } from "react-native";
|
||||
import { percent, useYoshiki, vh } from "yoshiki/native";
|
||||
import { TransparentLayout } from "../layout";
|
||||
import { DefaultLayout } from "../layout";
|
||||
import { EpisodeList, SeasonTab } from "./season";
|
||||
import { Header } from "./header";
|
||||
import Svg, { Path, SvgProps } from "react-native-svg";
|
||||
@ -89,7 +89,7 @@ export const ShowDetails: QueryPage<{ slug: string; season: string }> = ({ slug,
|
||||
ShowDetails.getFetchUrls = ({ slug, season = 1 }) => [
|
||||
query(slug),
|
||||
// ShowStaff.query(slug),
|
||||
// EpisodeGrid.query(slug, season),
|
||||
EpisodeList.query(slug, season),
|
||||
];
|
||||
|
||||
ShowDetails.getLayout = TransparentLayout;
|
||||
ShowDetails.getLayout = { Layout: DefaultLayout, props: { transparent: true }};
|
||||
|
@ -27,6 +27,7 @@ import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
|
||||
export const InfiniteFetch = <Data,>({
|
||||
query,
|
||||
placeholderCount = 15,
|
||||
suspense = false,
|
||||
horizontal = false,
|
||||
children,
|
||||
layout,
|
||||
@ -44,14 +45,20 @@ export const InfiniteFetch = <Data,>({
|
||||
i: number,
|
||||
) => ReactElement | null;
|
||||
empty?: string | JSX.Element;
|
||||
suspense?: boolean;
|
||||
divider?: boolean | ComponentType;
|
||||
Header?: ComponentType<{ children: JSX.Element }>;
|
||||
}): JSX.Element | null => {
|
||||
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
|
||||
|
||||
const { numColumns, size } = useBreakpointMap(layout);
|
||||
const { items, error, fetchNextPage, hasNextPage, refetch, isRefetching } =
|
||||
useInfiniteFetch(query);
|
||||
const { items, error, fetchNextPage, hasNextPage, refetch, isRefetching } = useInfiniteFetch(
|
||||
query,
|
||||
{
|
||||
suspense: suspense,
|
||||
useErrorBoundary: false,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) return <ErrorView error={error} />;
|
||||
if (empty && items && items.length === 0) {
|
||||
|
@ -20,7 +20,7 @@
|
||||
|
||||
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
|
||||
import { HR } from "@kyoo/primitives";
|
||||
import { ComponentType, Fragment, ReactElement, useRef } from "react";
|
||||
import { ComponentType, Fragment, ReactElement, useMemo, useRef } from "react";
|
||||
import { Stylable, useYoshiki } from "yoshiki";
|
||||
import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
|
||||
|
||||
@ -88,6 +88,7 @@ const InfiniteScroll = ({
|
||||
|
||||
export const InfiniteFetch = <Data,>({
|
||||
query,
|
||||
suspense = false,
|
||||
placeholderCount = 15,
|
||||
children,
|
||||
layout,
|
||||
@ -98,6 +99,7 @@ export const InfiniteFetch = <Data,>({
|
||||
...props
|
||||
}: {
|
||||
query: QueryIdentifier<Data>;
|
||||
suspense?: boolean;
|
||||
placeholderCount?: number;
|
||||
layout: Layout;
|
||||
horizontal?: boolean;
|
||||
@ -111,7 +113,10 @@ export const InfiniteFetch = <Data,>({
|
||||
}): JSX.Element | null => {
|
||||
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
|
||||
|
||||
const { items, error, fetchNextPage, hasNextPage, isFetching } = useInfiniteFetch(query);
|
||||
const { items, error, fetchNextPage, hasNextPage, isFetching } = useInfiniteFetch(query, {
|
||||
suspense: suspense,
|
||||
useErrorBoundary: false,
|
||||
});
|
||||
const grid = layout.numColumns !== 1;
|
||||
|
||||
if (error) return <ErrorView error={error} />;
|
||||
|
@ -22,3 +22,4 @@ export * from "./navbar";
|
||||
export { BrowsePage } from "./browse";
|
||||
export { MovieDetails, ShowDetails } from "./details";
|
||||
export { Player } from "./player";
|
||||
export { SearchPage } from "./search";
|
||||
|
@ -23,39 +23,21 @@ import { Navbar } from "./navbar";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { Main } from "@kyoo/primitives";
|
||||
|
||||
export const DefaultLayout = ({ page }: { page: ReactElement }) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<Main
|
||||
{...css({
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
})}
|
||||
>
|
||||
{page}
|
||||
</Main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
DefaultLayout.getFetchUrls = () => [Navbar.query()];
|
||||
|
||||
export const TransparentLayout = ({ page }: { page: ReactElement }) => {
|
||||
export const DefaultLayout = ({ page, transparent }: { page: ReactElement, transparent?: boolean }) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar
|
||||
{...css({
|
||||
bg: "transparent",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
})}
|
||||
{...css(
|
||||
transparent && {
|
||||
bg: "transparent",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
<Main
|
||||
{...css({
|
||||
@ -69,4 +51,4 @@ export const TransparentLayout = ({ page }: { page: ReactElement }) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
TransparentLayout.getFetchUrls = () => [Navbar.query()];
|
||||
DefaultLayout.getFetchUrls = () => [Navbar.query()];
|
||||
|
@ -19,13 +19,27 @@
|
||||
*/
|
||||
|
||||
import { Library, LibraryP, Page, Paged, QueryIdentifier } from "@kyoo/models";
|
||||
import { IconButton, Header, Avatar, A, Skeleton, tooltip, ts } from "@kyoo/primitives";
|
||||
import { View } from "react-native";
|
||||
import {
|
||||
Input,
|
||||
IconButton,
|
||||
Header,
|
||||
Avatar,
|
||||
A,
|
||||
Skeleton,
|
||||
tooltip,
|
||||
ts,
|
||||
Link,
|
||||
} from "@kyoo/primitives";
|
||||
import { Platform, View } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createParam } from "solito";
|
||||
import { useRouter } from "solito/router";
|
||||
import { rem, Stylable, useTheme, useYoshiki } from "yoshiki/native";
|
||||
import Menu from "@material-symbols/svg-400/rounded/menu-fill.svg";
|
||||
import Search from "@material-symbols/svg-400/rounded/search-fill.svg";
|
||||
import { Fetch } from "../fetch";
|
||||
import { KyooLongLogo } from "./icon";
|
||||
import { useState } from "react";
|
||||
|
||||
export const NavbarTitle = (props: Stylable) => {
|
||||
const { t } = useTranslation();
|
||||
@ -37,14 +51,51 @@ export const NavbarTitle = (props: Stylable) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const LoginAvatar = (props: Stylable) => {
|
||||
const { useParam } = createParam<{ q?: string }>();
|
||||
|
||||
const SearchBar = () => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const { push, replace, back } = useRouter();
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
// const [query, setQuery] = Platform.OS === "web" ? useState("") : useParam("q");
|
||||
const [query, setQuery] = useParam("q");
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(q) => {
|
||||
setQuery(q);
|
||||
if (Platform.OS === "web") {
|
||||
const action = window.location.pathname.startsWith("/search") ? replace : push;
|
||||
if (q) action(`/search?q=${q}`, undefined, { shallow: true });
|
||||
else back();
|
||||
}
|
||||
}}
|
||||
placeholder={t("navbar.search")}
|
||||
placeholderTextColor={theme.light.overlay0}
|
||||
{...tooltip(t("navbar.search"))}
|
||||
{...css({ borderColor: (theme) => theme.colors.white })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Right = () => {
|
||||
const theme = useTheme();
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<A href="/auth/login" {...tooltip(t("navbar.login"))}>
|
||||
<Avatar alt={t("navbar.login")} size={30} color={theme.colors.white} />
|
||||
</A>
|
||||
<>
|
||||
{Platform.OS === "web" ? (
|
||||
<SearchBar />
|
||||
) : (
|
||||
<IconButton icon={Search} as={Link} href="/search" {...tooltip("navbar.search")} />
|
||||
)}
|
||||
<A href="/auth/login" {...tooltip(t("navbar.login"))} {...css({ marginLeft: ts(1) })}>
|
||||
<Avatar alt={t("navbar.login")} size={30} color={theme.colors.white} />
|
||||
</A>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -112,7 +163,7 @@ export const Navbar = (props: Stylable) => {
|
||||
}
|
||||
</Fetch>
|
||||
</View>
|
||||
<LoginAvatar />
|
||||
<Right />
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
70
front/packages/ui/src/search/index.tsx
Normal file
70
front/packages/ui/src/search/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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 { LibraryItem, LibraryItemP, QueryIdentifier, QueryPage } from "@kyoo/models";
|
||||
import { Suspense, useRef, useDeferredValue } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ItemGrid } from "../browse/grid";
|
||||
import { itemMap } from "../browse/index";
|
||||
import { EmptyView } from "../fetch";
|
||||
import { InfiniteFetch } from "../fetch-infinite";
|
||||
import { DefaultLayout } from "../layout";
|
||||
|
||||
const useIsFirstRender = () => {
|
||||
const isFirst = useRef(true);
|
||||
|
||||
if (isFirst.current) {
|
||||
isFirst.current = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const query = (query: string): QueryIdentifier<LibraryItem> => ({
|
||||
parser: LibraryItemP,
|
||||
path: ["search", query, "items"],
|
||||
infinite: true,
|
||||
getNext: () => undefined,
|
||||
});
|
||||
|
||||
export const SearchPage: QueryPage<{ q?: string }> = ({ q }) => {
|
||||
const deferredQuery = useDeferredValue(q);
|
||||
const { t } = useTranslation();
|
||||
const isFirst = useIsFirstRender();
|
||||
|
||||
const empty = <EmptyView message={t("search.empty")} />;
|
||||
if (!deferredQuery) return empty;
|
||||
return (
|
||||
<Suspense>
|
||||
<InfiniteFetch
|
||||
query={query(deferredQuery)}
|
||||
suspense={!isFirst}
|
||||
layout={ItemGrid.layout}
|
||||
placeholderCount={15}
|
||||
empty={empty}
|
||||
>
|
||||
{(item) => <ItemGrid {...itemMap(item)} />}
|
||||
</InfiniteFetch>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
SearchPage.getLayout = DefaultLayout;
|
||||
SearchPage.getFetchUrls = ({ q }) => (q ? [query(q)] : []);
|
@ -32,6 +32,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"home": "Home",
|
||||
"search": "Search",
|
||||
"login": "Login"
|
||||
},
|
||||
"player": {
|
||||
@ -45,5 +46,8 @@
|
||||
"subtitles": "Subtitles",
|
||||
"subtitle-none": "None",
|
||||
"fullscreen": "Fullscreen"
|
||||
},
|
||||
"search": {
|
||||
"empty": "No result found. Try a different query."
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@
|
||||
},
|
||||
"navbar": {
|
||||
"home": "Accueil",
|
||||
"search": "Rechercher",
|
||||
"login": "Connexion"
|
||||
},
|
||||
"player": {
|
||||
@ -45,5 +46,8 @@
|
||||
"subtitles": "Sous titres",
|
||||
"subtitle-none": "Aucun",
|
||||
"fullscreen": "Plein-écran"
|
||||
},
|
||||
"search": {
|
||||
"empty": "Aucun résultat trouvé. Essayer avec une autre recherche."
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user