mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-02 21:24:20 -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 { Stack } from "expo-router";
|
||||||
import { ThemeSelector } from "@kyoo/primitives";
|
import { ThemeSelector } from "@kyoo/primitives";
|
||||||
import { useTheme } from "yoshiki/native";
|
import { useTheme } from "yoshiki/native";
|
||||||
import { LoginAvatar, NavbarTitle } from "@kyoo/ui";
|
import { NavbarRight, NavbarTitle } from "@kyoo/ui";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { createQueryClient } from "@kyoo/models";
|
import { createQueryClient } from "@kyoo/models";
|
||||||
@ -54,7 +54,7 @@ const ThemedStack = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerTitle: () => <NavbarTitle />,
|
headerTitle: () => <NavbarTitle />,
|
||||||
headerRight: () => <LoginAvatar />,
|
headerRight: () => <NavbarRight />,
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: theme.appbar,
|
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 "../polyfill";
|
||||||
|
|
||||||
import { createTheme, ThemeProvider as MTheme } from "@mui/material";
|
|
||||||
import { Hydrate, QueryClientProvider } from "@tanstack/react-query";
|
import { Hydrate, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import NextApp, { AppContext, type AppProps } from "next/app";
|
import NextApp, { AppContext, type AppProps } from "next/app";
|
||||||
@ -92,7 +91,9 @@ const GlobalCssTheme = () => {
|
|||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
const { queryState, ...props } = superjson.deserialize<any>(pageProps ?? { json: {} });
|
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();
|
useMobileHover();
|
||||||
|
|
||||||
@ -105,7 +106,7 @@ const App = ({ Component, pageProps }: AppProps) => {
|
|||||||
<Hydrate state={queryState}>
|
<Hydrate state={queryState}>
|
||||||
<ThemeSelector>
|
<ThemeSelector>
|
||||||
<GlobalCssTheme />
|
<GlobalCssTheme />
|
||||||
<Layout page={<Component {...props} />} />
|
<Layout page={<Component {...props} />} {...layoutProps} />
|
||||||
</ThemeSelector>
|
</ThemeSelector>
|
||||||
</Hydrate>
|
</Hydrate>
|
||||||
</QueryClientProvider>
|
</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/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ComponentType, ReactElement, ReactNode } from "react";
|
import { ComponentProps, ComponentType, ReactElement } from "react";
|
||||||
import {
|
import {
|
||||||
dehydrate,
|
dehydrate,
|
||||||
QueryClient,
|
QueryClient,
|
||||||
QueryFunctionContext,
|
QueryFunctionContext,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
|
UseInfiniteQueryOptions,
|
||||||
useQuery,
|
useQuery,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -40,8 +41,8 @@ const queryFn = async <Data,>(
|
|||||||
? process.env.PUBLIC_BACK_URL
|
? process.env.PUBLIC_BACK_URL
|
||||||
: typeof window === "undefined"
|
: typeof window === "undefined"
|
||||||
? process.env.KYOO_URL ?? "http://localhost:5000"
|
? process.env.KYOO_URL ?? "http://localhost:5000"
|
||||||
// TODO remove the hardcoded fallback. This is just for testing purposes
|
: // TODO remove the hardcoded fallback. This is just for testing purposes
|
||||||
: "/api") ?? "https://beta.sdg.moe";
|
"/api") ?? "https://beta.sdg.moe";
|
||||||
if (!kyooUrl) console.error("Kyoo's url is not defined.");
|
if (!kyooUrl) console.error("Kyoo's url is not defined.");
|
||||||
|
|
||||||
let resp;
|
let resp;
|
||||||
@ -105,11 +106,17 @@ export type QueryIdentifier<T = unknown> = {
|
|||||||
path: (string | undefined)[];
|
path: (string | undefined)[];
|
||||||
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
||||||
infinite?: boolean;
|
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> & {
|
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
||||||
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
|
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>) => {
|
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>({
|
const ret = useInfiniteQuery<Page<Data>, KyooErrors>({
|
||||||
queryKey: toQueryKey(query),
|
queryKey: toQueryKey(query),
|
||||||
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
|
queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
|
||||||
|
@ -32,6 +32,7 @@ export * from "./divider";
|
|||||||
export * from "./progress";
|
export * from "./progress";
|
||||||
export * from "./slider";
|
export * from "./slider";
|
||||||
export * from "./menu";
|
export * from "./menu";
|
||||||
|
export * from "./input";
|
||||||
|
|
||||||
export * from "./animated";
|
export * from "./animated";
|
||||||
export * from "./utils";
|
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 { ItemList } from "./list";
|
||||||
import { SortBy, SortOrd, Layout } from "./types";
|
import { SortBy, SortOrd, Layout } from "./types";
|
||||||
|
|
||||||
const itemMap = (
|
export const itemMap = (
|
||||||
item: WithLoading<LibraryItem>,
|
item: WithLoading<LibraryItem>,
|
||||||
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
|
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
|
||||||
if (item.isLoading) return item;
|
if (item.isLoading) return item;
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
import { Movie, MovieP, QueryIdentifier, QueryPage } from "@kyoo/models";
|
import { Movie, MovieP, QueryIdentifier, QueryPage } from "@kyoo/models";
|
||||||
import { Platform, ScrollView } from "react-native";
|
import { Platform, ScrollView } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { TransparentLayout } from "../layout";
|
import { DefaultLayout } from "../layout";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
|
|
||||||
const query = (slug: string): QueryIdentifier<Movie> => ({
|
const query = (slug: string): QueryIdentifier<Movie> => ({
|
||||||
@ -48,4 +48,4 @@ MovieDetails.getFetchUrls = ({ slug }) => [
|
|||||||
// ShowStaff.query(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 { 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 { percent, useYoshiki, vh } from "yoshiki/native";
|
||||||
import { TransparentLayout } from "../layout";
|
import { DefaultLayout } from "../layout";
|
||||||
import { EpisodeList, SeasonTab } from "./season";
|
import { EpisodeList, SeasonTab } from "./season";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
import Svg, { Path, SvgProps } from "react-native-svg";
|
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 }) => [
|
ShowDetails.getFetchUrls = ({ slug, season = 1 }) => [
|
||||||
query(slug),
|
query(slug),
|
||||||
// ShowStaff.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,>({
|
export const InfiniteFetch = <Data,>({
|
||||||
query,
|
query,
|
||||||
placeholderCount = 15,
|
placeholderCount = 15,
|
||||||
|
suspense = false,
|
||||||
horizontal = false,
|
horizontal = false,
|
||||||
children,
|
children,
|
||||||
layout,
|
layout,
|
||||||
@ -44,14 +45,20 @@ export const InfiniteFetch = <Data,>({
|
|||||||
i: number,
|
i: number,
|
||||||
) => ReactElement | null;
|
) => ReactElement | null;
|
||||||
empty?: string | JSX.Element;
|
empty?: string | JSX.Element;
|
||||||
|
suspense?: boolean;
|
||||||
divider?: boolean | ComponentType;
|
divider?: boolean | ComponentType;
|
||||||
Header?: ComponentType<{ children: JSX.Element }>;
|
Header?: ComponentType<{ children: JSX.Element }>;
|
||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
|
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
|
||||||
|
|
||||||
const { numColumns, size } = useBreakpointMap(layout);
|
const { numColumns, size } = useBreakpointMap(layout);
|
||||||
const { items, error, fetchNextPage, hasNextPage, refetch, isRefetching } =
|
const { items, error, fetchNextPage, hasNextPage, refetch, isRefetching } = useInfiniteFetch(
|
||||||
useInfiniteFetch(query);
|
query,
|
||||||
|
{
|
||||||
|
suspense: suspense,
|
||||||
|
useErrorBoundary: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (error) return <ErrorView error={error} />;
|
if (error) return <ErrorView error={error} />;
|
||||||
if (empty && items && items.length === 0) {
|
if (empty && items && items.length === 0) {
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
|
import { Page, QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
|
||||||
import { HR } from "@kyoo/primitives";
|
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 { Stylable, useYoshiki } from "yoshiki";
|
||||||
import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
|
import { EmptyView, ErrorView, Layout, WithLoading } from "./fetch";
|
||||||
|
|
||||||
@ -88,6 +88,7 @@ const InfiniteScroll = ({
|
|||||||
|
|
||||||
export const InfiniteFetch = <Data,>({
|
export const InfiniteFetch = <Data,>({
|
||||||
query,
|
query,
|
||||||
|
suspense = false,
|
||||||
placeholderCount = 15,
|
placeholderCount = 15,
|
||||||
children,
|
children,
|
||||||
layout,
|
layout,
|
||||||
@ -98,6 +99,7 @@ export const InfiniteFetch = <Data,>({
|
|||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
query: QueryIdentifier<Data>;
|
query: QueryIdentifier<Data>;
|
||||||
|
suspense?: boolean;
|
||||||
placeholderCount?: number;
|
placeholderCount?: number;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
horizontal?: boolean;
|
horizontal?: boolean;
|
||||||
@ -111,7 +113,10 @@ export const InfiniteFetch = <Data,>({
|
|||||||
}): JSX.Element | null => {
|
}): JSX.Element | null => {
|
||||||
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
|
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;
|
const grid = layout.numColumns !== 1;
|
||||||
|
|
||||||
if (error) return <ErrorView error={error} />;
|
if (error) return <ErrorView error={error} />;
|
||||||
|
@ -22,3 +22,4 @@ export * from "./navbar";
|
|||||||
export { BrowsePage } from "./browse";
|
export { BrowsePage } from "./browse";
|
||||||
export { MovieDetails, ShowDetails } from "./details";
|
export { MovieDetails, ShowDetails } from "./details";
|
||||||
export { Player } from "./player";
|
export { Player } from "./player";
|
||||||
|
export { SearchPage } from "./search";
|
||||||
|
@ -23,39 +23,21 @@ import { Navbar } from "./navbar";
|
|||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { Main } from "@kyoo/primitives";
|
import { Main } from "@kyoo/primitives";
|
||||||
|
|
||||||
export const DefaultLayout = ({ page }: { page: ReactElement }) => {
|
export const DefaultLayout = ({ page, transparent }: { page: ReactElement, transparent?: boolean }) => {
|
||||||
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 }) => {
|
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar
|
<Navbar
|
||||||
{...css({
|
{...css(
|
||||||
|
transparent && {
|
||||||
bg: "transparent",
|
bg: "transparent",
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Main
|
<Main
|
||||||
{...css({
|
{...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 { Library, LibraryP, Page, Paged, QueryIdentifier } from "@kyoo/models";
|
||||||
import { IconButton, Header, Avatar, A, Skeleton, tooltip, ts } from "@kyoo/primitives";
|
import {
|
||||||
import { View } from "react-native";
|
Input,
|
||||||
|
IconButton,
|
||||||
|
Header,
|
||||||
|
Avatar,
|
||||||
|
A,
|
||||||
|
Skeleton,
|
||||||
|
tooltip,
|
||||||
|
ts,
|
||||||
|
Link,
|
||||||
|
} from "@kyoo/primitives";
|
||||||
|
import { Platform, View } from "react-native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { createParam } from "solito";
|
||||||
|
import { useRouter } from "solito/router";
|
||||||
import { rem, Stylable, useTheme, useYoshiki } from "yoshiki/native";
|
import { rem, Stylable, useTheme, useYoshiki } from "yoshiki/native";
|
||||||
import Menu from "@material-symbols/svg-400/rounded/menu-fill.svg";
|
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 { Fetch } from "../fetch";
|
||||||
import { KyooLongLogo } from "./icon";
|
import { KyooLongLogo } from "./icon";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export const NavbarTitle = (props: Stylable) => {
|
export const NavbarTitle = (props: Stylable) => {
|
||||||
const { t } = useTranslation();
|
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 theme = useTheme();
|
||||||
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<A href="/auth/login" {...tooltip(t("navbar.login"))}>
|
<>
|
||||||
|
{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} />
|
<Avatar alt={t("navbar.login")} size={30} color={theme.colors.white} />
|
||||||
</A>
|
</A>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,7 +163,7 @@ export const Navbar = (props: Stylable) => {
|
|||||||
}
|
}
|
||||||
</Fetch>
|
</Fetch>
|
||||||
</View>
|
</View>
|
||||||
<LoginAvatar />
|
<Right />
|
||||||
</Header>
|
</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": {
|
"navbar": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
|
"search": "Search",
|
||||||
"login": "Login"
|
"login": "Login"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@ -45,5 +46,8 @@
|
|||||||
"subtitles": "Subtitles",
|
"subtitles": "Subtitles",
|
||||||
"subtitle-none": "None",
|
"subtitle-none": "None",
|
||||||
"fullscreen": "Fullscreen"
|
"fullscreen": "Fullscreen"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"empty": "No result found. Try a different query."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"home": "Accueil",
|
"home": "Accueil",
|
||||||
|
"search": "Rechercher",
|
||||||
"login": "Connexion"
|
"login": "Connexion"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
@ -45,5 +46,8 @@
|
|||||||
"subtitles": "Sous titres",
|
"subtitles": "Sous titres",
|
||||||
"subtitle-none": "Aucun",
|
"subtitle-none": "Aucun",
|
||||||
"fullscreen": "Plein-écran"
|
"fullscreen": "Plein-écran"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"empty": "Aucun résultat trouvé. Essayer avec une autre recherche."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user