mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add image and poster component
This commit is contained in:
parent
50507a1379
commit
090d613266
@ -77,7 +77,7 @@ export const Navbar = () => {
|
|||||||
const { data, error, isSuccess, isError } = useFetch<Page<Library>>("libraries");
|
const { data, error, isSuccess, isError } = useFetch<Page<Library>>("libraries");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar position="static">
|
<AppBar position="sticky">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="large"
|
size="large"
|
||||||
|
128
front/src/components/poster.tsx
Normal file
128
front/src/components/poster.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box, NoSsr, Skeleton, styled } from "@mui/material";
|
||||||
|
import {
|
||||||
|
MutableRefObject,
|
||||||
|
SyntheticEvent,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { ComponentsOverrides, ComponentsProps, ComponentsVariants } from "@mui/material";
|
||||||
|
import { withThemeProps } from "~/utils/with-theme";
|
||||||
|
|
||||||
|
type ImageOptions = {
|
||||||
|
radius?: string;
|
||||||
|
fallback?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImageProps = {
|
||||||
|
img?: string;
|
||||||
|
alt: string;
|
||||||
|
} & ImageOptions;
|
||||||
|
|
||||||
|
type ImagePropsWithLoading =
|
||||||
|
| (ImageProps & { loading?: false })
|
||||||
|
| (Partial<ImageProps> & { loading: true });
|
||||||
|
|
||||||
|
const _Image = ({
|
||||||
|
img,
|
||||||
|
alt,
|
||||||
|
radius,
|
||||||
|
fallback,
|
||||||
|
loading = false,
|
||||||
|
aspectRatio = undefined,
|
||||||
|
width = undefined,
|
||||||
|
height = undefined,
|
||||||
|
...others
|
||||||
|
}: ImagePropsWithLoading &
|
||||||
|
(
|
||||||
|
| { aspectRatio?: string; width: string | number; height: string | number }
|
||||||
|
| { aspectRatio: string; width?: string | number; height?: string | number }
|
||||||
|
)) => {
|
||||||
|
const [showLoading, setLoading] = useState<boolean>(loading);
|
||||||
|
const imgRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
// This allow the loading bool to be false with SSR but still be on client-side
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!imgRef.current?.complete) setLoading(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
borderRadius={radius}
|
||||||
|
overflow={"hidden"}
|
||||||
|
sx={{
|
||||||
|
aspectRatio,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
backgroundColor: "primary.dark",
|
||||||
|
"& > *": { width: "100%", height: "100%" },
|
||||||
|
}}
|
||||||
|
{...others}
|
||||||
|
>
|
||||||
|
{showLoading && <Skeleton variant="rectangular" height="100%" />}
|
||||||
|
{!loading && img && (
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
ref={imgRef}
|
||||||
|
src={img}
|
||||||
|
alt={alt}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
onError={({ currentTarget }: SyntheticEvent<HTMLImageElement>) => {
|
||||||
|
if (fallback && currentTarget.src !== fallback) currentTarget.src = fallback;
|
||||||
|
else setLoading(false);
|
||||||
|
}}
|
||||||
|
sx={{ objectFit: "cover", display: showLoading ? "hidden" : undefined }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Image = styled(_Image)({});
|
||||||
|
|
||||||
|
// eslint-disable-next-line jsx-a11y/alt-text
|
||||||
|
const _Poster = (props: ImagePropsWithLoading) => <_Image aspectRatio="2 / 3" {...props} />;
|
||||||
|
|
||||||
|
declare module "@mui/material/styles" {
|
||||||
|
interface ComponentsPropsList {
|
||||||
|
Poster: ImageOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ComponentNameToClassKey {
|
||||||
|
Poster: Record<string, never>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Components<Theme = unknown> {
|
||||||
|
Poster?: {
|
||||||
|
defaultProps?: ComponentsProps["Poster"];
|
||||||
|
styleOverrides?: ComponentsOverrides<Theme>["Poster"];
|
||||||
|
variants?: ComponentsVariants["Poster"];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Poster = withThemeProps(_Poster, {
|
||||||
|
name: "Poster",
|
||||||
|
slot: "Root",
|
||||||
|
});
|
@ -27,18 +27,31 @@ import { Hydrate, QueryClientProvider } from "react-query";
|
|||||||
import { createQueryClient, fetchQuery } from "~/utils/query";
|
import { createQueryClient, fetchQuery } from "~/utils/query";
|
||||||
import { defaultTheme } from "~/utils/themes/default-theme";
|
import { defaultTheme } from "~/utils/themes/default-theme";
|
||||||
import { Navbar } from "~/components/navbar";
|
import { Navbar } from "~/components/navbar";
|
||||||
import "../global.css"
|
import "../global.css";
|
||||||
|
import { Box } from "@mui/system";
|
||||||
|
|
||||||
|
const AppWithNavbar = ({ children }: { children: JSX.Element }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Navbar/>
|
||||||
|
{/* TODO: add an option to disable the navbar in the component */}
|
||||||
|
<Box >
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
const [queryClient] = useState(() => createQueryClient());
|
const [queryClient] = useState(() => createQueryClient());
|
||||||
|
const { queryState, ...props } = pageProps;
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Hydrate state={pageProps.queryState}>
|
<Hydrate state={queryState}>
|
||||||
<ThemeProvider theme={defaultTheme}>
|
<ThemeProvider theme={defaultTheme}>
|
||||||
<Navbar />
|
<AppWithNavbar>
|
||||||
{/* TODO: add a container to allow the component to be scrolled without the navbar */}
|
<Component {...props} />
|
||||||
{/* TODO: add an option to disable the navbar in the component */}
|
</AppWithNavbar>
|
||||||
<Component {...pageProps} />
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Hydrate>
|
</Hydrate>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
@ -49,7 +62,7 @@ App.getInitialProps = async (ctx: AppContext) => {
|
|||||||
const appProps = await NextApp.getInitialProps(ctx);
|
const appProps = await NextApp.getInitialProps(ctx);
|
||||||
|
|
||||||
const getUrl = (ctx.Component as any).getFetchUrls;
|
const getUrl = (ctx.Component as any).getFetchUrls;
|
||||||
const urls: [[string]] = getUrl ? getUrl(ctx.router.query) : [];
|
const urls: string[][] = getUrl ? getUrl(ctx.router.query) : [];
|
||||||
// TODO: check if the navbar is needed for this
|
// TODO: check if the navbar is needed for this
|
||||||
urls.push(["libraries"]);
|
urls.push(["libraries"]);
|
||||||
appProps.pageProps.queryState = await fetchQuery(urls);
|
appProps.pageProps.queryState = await fetchQuery(urls);
|
||||||
|
53
front/src/pages/show/[slug].tsx
Normal file
53
front/src/pages/show/[slug].tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Box, Typography } from "@mui/material";
|
||||||
|
import { Image, Poster } from "~/components/poster";
|
||||||
|
import { Show } from "~/models";
|
||||||
|
import { QueryPage, useFetch } from "~/utils/query";
|
||||||
|
import { withRoute } from "~/utils/router";
|
||||||
|
|
||||||
|
const ShowHeader = (data: Show) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Image img={data.thumbnail} alt="" height="60vh" width="100%" sx={{ positon: "relative" }} />
|
||||||
|
<Poster img={data.poster} alt={`${data.name}`} />
|
||||||
|
<Typography variant="h1" component="h1">
|
||||||
|
{data.name}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ShowDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||||
|
const { data } = useFetch<Show>("shows", slug);
|
||||||
|
|
||||||
|
if (!data) return <p>oups</p>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ShowHeader {...data} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ShowDetails.getFetchUrls = ({ slug }) => [["shows", slug]];
|
||||||
|
|
||||||
|
export default withRoute(ShowDetails);
|
@ -59,27 +59,31 @@ export const createQueryClient = () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
export type QueryPage<Props = {}> = ComponentType<Props> & {
|
||||||
getFetchUrls?: (route: { [key: string]: string }) => [[string]];
|
getFetchUrls?: (route: { [key: string]: string }) => string[][];
|
||||||
};
|
};
|
||||||
|
|
||||||
const imageSelector = <T>(obj: T): T => {
|
const imageSelector = <T>(obj: T): T => {
|
||||||
|
// TODO: remove this
|
||||||
|
// @ts-ignore
|
||||||
|
if ("title" in obj) obj.name = obj.title;
|
||||||
|
|
||||||
for (const img of imageList) {
|
for (const img of imageList) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (img in obj && !obj[img].startWith("/api")) {
|
if (img in obj && obj[img] && !obj[img].startsWith("/api")) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
obj[img] = `/api/${obj[img]}`;
|
obj[img] = `/api${obj[img]}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFetch = <Data>(...params: [string]) => {
|
export const useFetch = <Data>(...params: string[]) => {
|
||||||
return useQuery<Data, KyooErrors>(params, {
|
return useQuery<Data, KyooErrors>(params, {
|
||||||
select: imageSelector,
|
select: imageSelector,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useInfiniteFetch = <Data>(...params: [string]) => {
|
export const useInfiniteFetch = <Data>(...params: string[]) => {
|
||||||
return useInfiniteQuery<Page<Data>, KyooErrors>(params, {
|
return useInfiniteQuery<Page<Data>, KyooErrors>(params, {
|
||||||
select: (pages) => {
|
select: (pages) => {
|
||||||
pages.pages.map((x) => x.items.map(imageSelector));
|
pages.pages.map((x) => x.items.map(imageSelector));
|
||||||
@ -88,10 +92,11 @@ export const useInfiniteFetch = <Data>(...params: [string]) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchQuery = async (queries: [[string]]) => {
|
export const fetchQuery = async (queries: string[][]) => {
|
||||||
// we can't put this check in a function because we want build time optimizations
|
// we can't put this check in a function because we want build time optimizations
|
||||||
// see https://github.com/vercel/next.js/issues/5354 for details
|
// see https://github.com/vercel/next.js/issues/5354 for details
|
||||||
if (typeof window !== "undefined") return {};
|
if (typeof window !== "undefined") return {};
|
||||||
|
console.log(queries)
|
||||||
|
|
||||||
const client = createQueryClient();
|
const client = createQueryClient();
|
||||||
await Promise.all(queries.map((x) => client.prefetchQuery(x)));
|
await Promise.all(queries.map((x) => client.prefetchQuery(x)));
|
||||||
|
34
front/src/utils/router.tsx
Normal file
34
front/src/utils/router.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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 { useRouter } from "next/router";
|
||||||
|
import { ComponentType } from "react";
|
||||||
|
|
||||||
|
export const withRoute = <Props,>(Component: ComponentType<Props>) => {
|
||||||
|
const WithUseRoute = (props: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
return <Component {...router.query} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { ...all } = Component;
|
||||||
|
Object.assign(WithUseRoute, { ...all });
|
||||||
|
return WithUseRoute;
|
||||||
|
};
|
56
front/src/utils/with-theme.tsx
Normal file
56
front/src/utils/with-theme.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 { Theme, useThemeProps, styled } from "@mui/material";
|
||||||
|
import { MUIStyledCommonProps, MuiStyledOptions } from "@mui/system";
|
||||||
|
import { FilteringStyledOptions } from "@mui/styled-engine";
|
||||||
|
import { WithConditionalCSSProp } from "@emotion/react/types/jsx-namespace";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
export interface ClassNameProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withThemeProps = <P,>(
|
||||||
|
component: React.ComponentType<P>,
|
||||||
|
options?: FilteringStyledOptions<P> & MuiStyledOptions,
|
||||||
|
) => {
|
||||||
|
const name = options?.name || component.displayName;
|
||||||
|
const Component = styled(component, options)<P>(() => ({}));
|
||||||
|
|
||||||
|
const WithTheme = (
|
||||||
|
inProps: P &
|
||||||
|
WithConditionalCSSProp<P & MUIStyledCommonProps<Theme>> &
|
||||||
|
ClassNameProps &
|
||||||
|
MUIStyledCommonProps<Theme>,
|
||||||
|
) => {
|
||||||
|
if (!name) {
|
||||||
|
console.error(
|
||||||
|
"withTheme could not be defined because the underlining component does not have a display name and the name option was not specified.",
|
||||||
|
);
|
||||||
|
return <Component {...inProps} />;
|
||||||
|
}
|
||||||
|
const props = useThemeProps({ props: inProps, name: name });
|
||||||
|
const className = clsx(props.className, `${name}-${options?.slot ?? "Root"}`);
|
||||||
|
return <Component {...props} className={className} />;
|
||||||
|
};
|
||||||
|
WithTheme.displayName = `WithThemeProps(${name || "Component"})`;
|
||||||
|
return WithTheme;
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user