mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Rewrite the movie header
This commit is contained in:
parent
a213c39445
commit
e5b236f51c
@ -1,3 +1,23 @@
|
||||
/*
|
||||
* 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";
|
||||
|
||||
export default BrowsePage
|
||||
export default BrowsePage;
|
||||
|
23
front/apps/mobile/app/movie/index.tsx
Normal file
23
front/apps/mobile/app/movie/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 { MovieDetails } from "@kyoo/ui";
|
||||
|
||||
export default MovieDetails;
|
@ -31,7 +31,7 @@
|
||||
"react-native-safe-area-context": "4.4.1",
|
||||
"react-native-screens": "~3.18.0",
|
||||
"react-native-svg": "13.4.0",
|
||||
"yoshiki": "0.2.9"
|
||||
"yoshiki": "0.2.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
|
@ -41,7 +41,7 @@
|
||||
"react-native-web": "^0.18.10",
|
||||
"solito": "^2.0.5",
|
||||
"superjson": "^1.11.0",
|
||||
"yoshiki": "0.2.9",
|
||||
"yoshiki": "0.2.11",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -18,6 +18,7 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import BrowsePage from "./index";
|
||||
import { BrowsePage } from "@kyoo/ui";
|
||||
import { withRoute } from "~/utils/router";
|
||||
|
||||
export default BrowsePage;
|
||||
export default withRoute(BrowsePage);
|
||||
|
@ -22,423 +22,3 @@ import { BrowsePage } from "@kyoo/ui";
|
||||
import { withRoute } from "~/utils/router";
|
||||
|
||||
export default withRoute(BrowsePage);
|
||||
|
||||
/* import { FilterList, GridView, North, Sort, South, ViewList } from "@mui/icons-material"; */
|
||||
/* import { */
|
||||
/* Box, */
|
||||
/* Button, */
|
||||
/* ButtonGroup, */
|
||||
/* ListItemIcon, */
|
||||
/* ListItemText, */
|
||||
/* MenuItem, */
|
||||
/* Menu, */
|
||||
/* Skeleton, */
|
||||
/* Divider, */
|
||||
/* Tooltip, */
|
||||
/* Typography, */
|
||||
/* } from "@mui/material"; */
|
||||
/* import useTranslation from "next-translate/useTranslation"; */
|
||||
/* import { useRouter } from "next/router"; */
|
||||
/* import { useState } from "react"; */
|
||||
/* import { ErrorPage } from "~/components/errors"; */
|
||||
/* import { Navbar } from "@kyoo/ui"; */
|
||||
/* import { Poster, Image } from "@kyoo/primitives"; */
|
||||
/* import { ItemType, LibraryItem, LibraryItemP } from "~/models"; */
|
||||
/* import { getDisplayDate } from "@kyoo/models"; */
|
||||
/* import { InfiniteScroll } from "~/utils/infinite-scroll"; */
|
||||
/* import { Link } from "~/utils/link"; */
|
||||
/* import { withRoute } from "~/utils/router"; */
|
||||
/* import { QueryIdentifier, QueryPage, useInfiniteFetch } from "@kyoo/models"; */
|
||||
/* import { px } from "yoshiki/native"; */
|
||||
|
||||
/* enum SortBy { */
|
||||
/* Name = "name", */
|
||||
/* StartAir = "startAir", */
|
||||
/* EndAir = "endAir", */
|
||||
/* } */
|
||||
|
||||
/* enum SortOrd { */
|
||||
/* Asc = "asc", */
|
||||
/* Desc = "desc", */
|
||||
/* } */
|
||||
|
||||
/* enum Layout { */
|
||||
/* Grid, */
|
||||
/* List, */
|
||||
/* } */
|
||||
|
||||
/* const ItemGrid = ({ */
|
||||
/* href, */
|
||||
/* name, */
|
||||
/* subtitle, */
|
||||
/* poster, */
|
||||
/* loading, */
|
||||
/* }: { */
|
||||
/* href?: string; */
|
||||
/* name?: string; */
|
||||
/* subtitle?: string | null; */
|
||||
/* poster?: string | null; */
|
||||
/* loading?: boolean; */
|
||||
/* }) => { */
|
||||
/* return ( */
|
||||
/* <Link */
|
||||
/* href={href ?? ""} */
|
||||
/* color="inherit" */
|
||||
/* sx={{ */
|
||||
/* display: "flex", */
|
||||
/* alignItems: "center", */
|
||||
/* textAlign: "center", */
|
||||
/* width: ["18%", "25%"], */
|
||||
/* minWidth: ["90px", "120px"], */
|
||||
/* maxWidth: "168px", */
|
||||
/* flexDirection: "column", */
|
||||
/* m: [1, 2], */
|
||||
/* }} */
|
||||
/* > */
|
||||
/* <Poster src={poster} alt={name} width="100%" /> */
|
||||
/* <Typography minWidth="80%">{name ?? <Skeleton />}</Typography> */
|
||||
/* {(loading || subtitle) && ( */
|
||||
/* <Typography variant="caption" minWidth="50%"> */
|
||||
/* {subtitle ?? <Skeleton />} */
|
||||
/* </Typography> */
|
||||
/* )} */
|
||||
/* </Link> */
|
||||
/* ); */
|
||||
/* }; */
|
||||
|
||||
/* const ItemList = ({ */
|
||||
/* href, */
|
||||
/* name, */
|
||||
/* subtitle, */
|
||||
/* thumbnail, */
|
||||
/* poster, */
|
||||
/* loading, */
|
||||
/* }: { */
|
||||
/* href?: string; */
|
||||
/* name?: string; */
|
||||
/* subtitle?: string | null; */
|
||||
/* poster?: string | null; */
|
||||
/* thumbnail?: string | null; */
|
||||
/* loading?: boolean; */
|
||||
/* }) => { */
|
||||
/* return ( */
|
||||
/* <Link */
|
||||
/* href={href ?? ""} */
|
||||
/* color="inherit" */
|
||||
/* sx={{ */
|
||||
/* display: "flex", */
|
||||
/* textAlign: "center", */
|
||||
/* alignItems: "center", */
|
||||
/* justifyContent: "space-evenly", */
|
||||
/* width: "100%", */
|
||||
/* height: "300px", */
|
||||
/* flexDirection: "row", */
|
||||
/* m: 1, */
|
||||
/* position: "relative", */
|
||||
/* color: "white", */
|
||||
/* "&:hover .poster": { */
|
||||
/* transform: "scale(1.3)", */
|
||||
/* }, */
|
||||
/* }} */
|
||||
/* > */
|
||||
/* <Image */
|
||||
/* src={thumbnail} */
|
||||
/* alt={name} */
|
||||
/* width="100%" */
|
||||
/* height="100%" */
|
||||
/* radius={px(5)} */
|
||||
/* css={{ */
|
||||
/* position: "absolute", */
|
||||
/* top: 0, */
|
||||
/* bottom: 0, */
|
||||
/* left: 0, */
|
||||
/* right: 0, */
|
||||
/* zIndex: -1, */
|
||||
|
||||
/* "&::after": { */
|
||||
/* content: '""', */
|
||||
/* position: "absolute", */
|
||||
/* top: 0, */
|
||||
/* bottom: 0, */
|
||||
/* right: 0, */
|
||||
/* left: 0, */
|
||||
/* // background: "rgba(0, 0, 0, 0.4)", */
|
||||
/* background: "linear-gradient(to bottom, rgba(0, 0, 0, 0) 25%, rgba(0, 0, 0, 0.6) 100%)", */
|
||||
/* }, */
|
||||
/* }} */
|
||||
/* /> */
|
||||
/* <Box */
|
||||
/* sx={{ */
|
||||
/* display: "flex", */
|
||||
/* flexDirection: "column", */
|
||||
/* width: { xs: "50%", lg: "30%" }, */
|
||||
/* }} */
|
||||
/* > */
|
||||
/* <Typography */
|
||||
/* variant="button" */
|
||||
/* sx={{ */
|
||||
/* fontSize: "2rem", */
|
||||
/* letterSpacing: "0.002rem", */
|
||||
/* fontWeight: 900, */
|
||||
/* }} */
|
||||
/* > */
|
||||
/* {name ?? <Skeleton />} */
|
||||
/* </Typography> */
|
||||
/* {(loading || subtitle) && ( */
|
||||
/* <Typography variant="caption" sx={{ fontSize: "1rem" }}> */
|
||||
/* {subtitle ?? <Skeleton />} */
|
||||
/* </Typography> */
|
||||
/* )} */
|
||||
/* </Box> */
|
||||
/* <Poster */
|
||||
/* src={poster} */
|
||||
/* alt="" */
|
||||
/* height="80%" */
|
||||
/* css={{ */
|
||||
/* transition: "transform .2s", */
|
||||
/* }} */
|
||||
/* /> */
|
||||
/* </Link> */
|
||||
/* ); */
|
||||
/* }; */
|
||||
|
||||
/* const Item = ({ item, layout }: { item?: LibraryItem; layout: Layout }) => { */
|
||||
/* let href; */
|
||||
/* if (item?.type === ItemType.Movie) href = `/movie/${item.slug}`; */
|
||||
/* else if (item?.type === ItemType.Show) href = `/show/${item.slug}`; */
|
||||
/* else if (item?.type === ItemType.Collection) href = `/collection/${item.slug}`; */
|
||||
|
||||
/* switch (layout) { */
|
||||
/* case Layout.Grid: */
|
||||
/* return ( */
|
||||
/* <ItemGrid */
|
||||
/* href={href} */
|
||||
/* name={item?.name} */
|
||||
/* subtitle={item && item.type !== ItemType.Collection ? getDisplayDate(item) : null} */
|
||||
/* poster={item?.poster} */
|
||||
/* loading={!item} */
|
||||
/* /> */
|
||||
/* ); */
|
||||
/* case Layout.List: */
|
||||
/* return ( */
|
||||
/* <ItemList */
|
||||
/* href={href} */
|
||||
/* name={item?.name} */
|
||||
/* subtitle={item && item.type !== ItemType.Collection ? getDisplayDate(item) : null} */
|
||||
/* poster={item?.poster} */
|
||||
/* thumbnail={item?.thumbnail} */
|
||||
/* loading={!item} */
|
||||
/* /> */
|
||||
/* ); */
|
||||
/* } */
|
||||
/* }; */
|
||||
|
||||
/* const SortByMenu = ({ */
|
||||
/* sortKey, */
|
||||
/* setSort, */
|
||||
/* sortOrd, */
|
||||
/* setSortOrd, */
|
||||
/* anchor, */
|
||||
/* onClose, */
|
||||
/* }: { */
|
||||
/* sortKey: SortBy; */
|
||||
/* setSort: (sort: SortBy) => void; */
|
||||
/* sortOrd: SortOrd; */
|
||||
/* setSortOrd: (sort: SortOrd) => void; */
|
||||
/* anchor: HTMLElement; */
|
||||
/* onClose: () => void; */
|
||||
/* }) => { */
|
||||
/* const router = useRouter(); */
|
||||
/* const { t } = useTranslation("browse"); */
|
||||
|
||||
/* return ( */
|
||||
/* <Menu */
|
||||
/* id="sortby-menu" */
|
||||
/* MenuListProps={{ */
|
||||
/* "aria-labelledby": "sortby", */
|
||||
/* }} */
|
||||
/* anchorEl={anchor} */
|
||||
/* open={!!anchor} */
|
||||
/* onClose={onClose} */
|
||||
/* > */
|
||||
/* {Object.values(SortBy).map((x) => ( */
|
||||
/* <MenuItem */
|
||||
/* key={x} */
|
||||
/* selected={sortKey === x} */
|
||||
/* onClick={() => setSort(x)} */
|
||||
/* component={Link} */
|
||||
/* to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} */
|
||||
/* shallow */
|
||||
/* replace */
|
||||
/* > */
|
||||
/* <ListItemText>{t(`browse.sortkey.${x}`)}</ListItemText> */
|
||||
/* </MenuItem> */
|
||||
/* ))} */
|
||||
/* <Divider /> */
|
||||
/* <MenuItem */
|
||||
/* selected={sortOrd === SortOrd.Asc} */
|
||||
/* onClick={() => setSortOrd(SortOrd.Asc)} */
|
||||
/* component={Link} */
|
||||
/* to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} */
|
||||
/* shallow */
|
||||
/* replace */
|
||||
/* > */
|
||||
/* <ListItemIcon> */
|
||||
/* <South fontSize="small" /> */
|
||||
/* </ListItemIcon> */
|
||||
/* <ListItemText>{t("browse.sortord.asc")}</ListItemText> */
|
||||
/* </MenuItem> */
|
||||
/* <MenuItem */
|
||||
/* selected={sortOrd === SortOrd.Desc} */
|
||||
/* onClick={() => setSortOrd(SortOrd.Desc)} */
|
||||
/* component={Link} */
|
||||
/* to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} */
|
||||
/* shallow */
|
||||
/* replace */
|
||||
/* > */
|
||||
/* <ListItemIcon> */
|
||||
/* <North fontSize="small" /> */
|
||||
/* </ListItemIcon> */
|
||||
/* <ListItemText>{t("browse.sortord.desc")}</ListItemText> */
|
||||
/* </MenuItem> */
|
||||
/* </Menu> */
|
||||
/* ); */
|
||||
/* }; */
|
||||
|
||||
/* const BrowseSettings = ({ */
|
||||
/* sortKey, */
|
||||
/* setSort, */
|
||||
/* sortOrd, */
|
||||
/* setSortOrd, */
|
||||
/* layout, */
|
||||
/* setLayout, */
|
||||
/* }: { */
|
||||
/* sortKey: SortBy; */
|
||||
/* setSort: (sort: SortBy) => void; */
|
||||
/* sortOrd: SortOrd; */
|
||||
/* setSortOrd: (sort: SortOrd) => void; */
|
||||
/* layout: Layout; */
|
||||
/* setLayout: (layout: Layout) => void; */
|
||||
/* }) => { */
|
||||
/* const [sortAnchor, setSortAnchor] = useState<HTMLElement | null>(null); */
|
||||
/* const { t } = useTranslation("browse"); */
|
||||
|
||||
/* const switchViewTitle = */
|
||||
/* layout === Layout.Grid ? t("browse.switchToList") : t("browse.switchToGrid"); */
|
||||
|
||||
/* return ( */
|
||||
/* <> */
|
||||
/* <Box sx={{ display: "flex", justifyContent: "space-around" }}> */
|
||||
/* <ButtonGroup sx={{ m: 1 }}> */
|
||||
/* <Button disabled> */
|
||||
/* <FilterList /> */
|
||||
/* </Button> */
|
||||
/* <Tooltip title={t("browse.sortby-tt")}> */
|
||||
/* <Button */
|
||||
/* id="sortby" */
|
||||
/* aria-label={t("browse.sortby-tt")} */
|
||||
/* aria-controls={sortAnchor ? "sorby-menu" : undefined} */
|
||||
/* aria-haspopup="true" */
|
||||
/* aria-expanded={sortAnchor ? "true" : undefined} */
|
||||
/* onClick={(event) => setSortAnchor(event.currentTarget)} */
|
||||
/* > */
|
||||
/* <Sort /> */
|
||||
/* {t("browse.sortby", { key: t(`browse.sortkey.${sortKey}`) })} */
|
||||
/* {sortOrd === SortOrd.Asc ? <South fontSize="small" /> : <North fontSize="small" />} */
|
||||
/* </Button> */
|
||||
/* </Tooltip> */
|
||||
/* <Tooltip title={switchViewTitle}> */
|
||||
/* <Button */
|
||||
/* onClick={() => setLayout(layout === Layout.List ? Layout.Grid : Layout.List)} */
|
||||
/* aria-label={switchViewTitle} */
|
||||
/* > */
|
||||
/* {layout === Layout.List ? <GridView /> : <ViewList />} */
|
||||
/* </Button> */
|
||||
/* </Tooltip> */
|
||||
/* </ButtonGroup> */
|
||||
/* </Box> */
|
||||
/* {sortAnchor && ( */
|
||||
/* <SortByMenu */
|
||||
/* sortKey={sortKey} */
|
||||
/* sortOrd={sortOrd} */
|
||||
/* setSort={setSort} */
|
||||
/* setSortOrd={setSortOrd} */
|
||||
/* anchor={sortAnchor} */
|
||||
/* onClose={() => setSortAnchor(null)} */
|
||||
/* /> */
|
||||
/* )} */
|
||||
/* </> */
|
||||
/* ); */
|
||||
/* }; */
|
||||
|
||||
/* const query = ( */
|
||||
/* slug?: string, */
|
||||
/* sortKey?: SortBy, */
|
||||
/* sortOrd?: SortOrd, */
|
||||
/* ): QueryIdentifier<LibraryItem> => ({ */
|
||||
/* parser: LibraryItemP, */
|
||||
/* path: slug ? ["library", slug, "items"] : ["items"], */
|
||||
/* infinite: true, */
|
||||
/* params: { */
|
||||
/* // The API still uses title isntead of name */
|
||||
/* sortBy: sortKey */
|
||||
/* ? `${sortKey === SortBy.Name ? "title" : sortKey}:${sortOrd ?? "asc"}` */
|
||||
/* : "title:asc", */
|
||||
/* }, */
|
||||
/* }); */
|
||||
|
||||
/* const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => { */
|
||||
/* const [sortKey, setSort] = useState(SortBy.Name); */
|
||||
/* const [sortOrd, setSortOrd] = useState(SortOrd.Asc); */
|
||||
/* const [layout, setLayout] = useState(Layout.Grid); */
|
||||
/* const { items, fetchNextPage, hasNextPage, error } = useInfiniteFetch( */
|
||||
/* query(slug, sortKey, sortOrd), */
|
||||
/* ); */
|
||||
|
||||
/* if (error) return <ErrorPage {...error} />; */
|
||||
|
||||
/* return ( */
|
||||
/* <> */
|
||||
/* <BrowseSettings */
|
||||
/* sortKey={sortKey} */
|
||||
/* setSort={setSort} */
|
||||
/* sortOrd={sortOrd} */
|
||||
/* setSortOrd={setSortOrd} */
|
||||
/* layout={layout} */
|
||||
/* setLayout={setLayout} */
|
||||
/* /> */
|
||||
/* <InfiniteScroll */
|
||||
/* dataLength={items?.length ?? 0} */
|
||||
/* next={fetchNextPage} */
|
||||
/* hasMore={hasNextPage!} */
|
||||
/* loader={[...Array(12).map((_, i) => <Item key={i} layout={layout} />)]} */
|
||||
/* sx={{ */
|
||||
/* display: "flex", */
|
||||
/* flexWrap: "wrap", */
|
||||
/* alignItems: "flex-start", */
|
||||
/* justifyContent: "center", */
|
||||
/* }} */
|
||||
/* > */
|
||||
/* {(items ?? [...Array(12)]).map((x, i) => ( */
|
||||
/* <Item key={x?.id ?? i} item={x} layout={layout} /> */
|
||||
/* ))} */
|
||||
/* </InfiniteScroll> */
|
||||
/* </> */
|
||||
/* ); */
|
||||
/* }; */
|
||||
|
||||
/* BrowsePage.getLayout = (page) => { */
|
||||
/* return ( */
|
||||
/* <> */
|
||||
/* <Navbar /> */
|
||||
/* <main>{page}</main> */
|
||||
/* </> */
|
||||
/* ); */
|
||||
/* }; */
|
||||
|
||||
/* BrowsePage.getFetchUrls = ({ slug, sortBy }) => [ */
|
||||
/* query(slug, sortBy?.split("-")[0] as SortBy, sortBy?.split("-")[1] as SortOrd), */
|
||||
/* Navbar.query(), */
|
||||
/* ]; */
|
||||
|
||||
/* export default withRoute(BrowsePage); */
|
||||
|
@ -18,294 +18,7 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LocalMovies, PlayArrow } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Divider,
|
||||
Fab,
|
||||
IconButton,
|
||||
Skeleton,
|
||||
SxProps,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import Head from "next/head";
|
||||
import { Navbar } from "~/components/navbar";
|
||||
import { Image, Poster } from "~/components/poster";
|
||||
import { Movie, MovieP, Show } from "~/models";
|
||||
import { QueryIdentifier, QueryPage, useFetch, useInfiniteFetch } from "~/utils/query";
|
||||
import { getDisplayDate } from "~/models/utils";
|
||||
import { MovieDetails } from "@kyoo/ui";
|
||||
import { withRoute } from "~/utils/router";
|
||||
import { Container } from "~/components/container";
|
||||
import { makeTitle } from "~/utils/utils";
|
||||
import { Link } from "~/utils/link";
|
||||
import { Studio } from "~/models/resources/studio";
|
||||
import { Person, PersonP } from "~/models";
|
||||
import { PersonAvatar } from "~/components/person";
|
||||
import { ErrorComponent, ErrorPage } from "~/components/errors";
|
||||
import { HorizontalList } from "~/components/horizontal-list";
|
||||
import NextLink from "next/link";
|
||||
|
||||
const StudioText = ({
|
||||
studio,
|
||||
loading = false,
|
||||
sx,
|
||||
}: {
|
||||
studio?: Studio | null;
|
||||
loading?: boolean;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
if (!loading && !studio) return null;
|
||||
return (
|
||||
<Typography sx={sx}>
|
||||
{t("show.studio")}:{" "}
|
||||
{loading ? (
|
||||
<Skeleton width="5rem" sx={{ display: "inline-flex" }} />
|
||||
) : (
|
||||
<Link href={`/studio/${studio!.slug}`}>{studio!.name}</Link>
|
||||
)}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShowHeader = ({ data }: { data?: Show | Movie }) => {
|
||||
/* const scroll = useScroll(); */
|
||||
const { t } = useTranslation("browse");
|
||||
// TODO: tweek the navbar color with the theme.
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: Add a shadow on navbar items */}
|
||||
{/* TODO: Put the navbar outside of the scrollbox */}
|
||||
<Navbar
|
||||
position="fixed"
|
||||
elevation={0}
|
||||
sx={{ backgroundColor: `rgba(0, 0, 0, ${0.4 /*+ scroll / 1000*/})` }}
|
||||
/>
|
||||
<Image
|
||||
img={data?.thumbnail}
|
||||
alt=""
|
||||
loading={!data}
|
||||
width="100%"
|
||||
height={{ xs: "40vh", sm: "60vh", lg: "70vh" }}
|
||||
sx={{
|
||||
minHeight: { xs: "350px", sm: "400px", lg: "550px" },
|
||||
position: "relative",
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
background: "linear-gradient(to bottom, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.6) 100%)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Container
|
||||
sx={{
|
||||
position: "relative",
|
||||
marginTop: { xs: "-30%", sm: "-25%", md: "-15rem", lg: "-21rem", xl: "-23rem" },
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "center", sm: "unset" },
|
||||
textAlign: { xs: "center", sm: "unset" },
|
||||
}}
|
||||
>
|
||||
<Poster
|
||||
img={data?.poster}
|
||||
alt={data?.name ?? ""}
|
||||
loading={!data}
|
||||
width={{ xs: "50%", md: "25%" }}
|
||||
sx={{ maxWidth: { xs: "175px", sm: "unset" }, flexShrink: 0 }}
|
||||
/>
|
||||
<Box sx={{ alignSelf: { xs: "center", sm: "end", md: "center" }, pl: { sm: "2.5rem" } }}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h1"
|
||||
sx={{
|
||||
color: { md: "white" },
|
||||
fontWeight: { md: 900 },
|
||||
mb: ".5rem",
|
||||
}}
|
||||
>
|
||||
{data?.name ?? <Skeleton width="15rem" />}
|
||||
</Typography>
|
||||
{(!data || getDisplayDate(data)) && (
|
||||
<Typography
|
||||
component="p"
|
||||
variant="h5"
|
||||
sx={{ color: { md: "white" }, fontWeight: 300, mb: ".5rem" }}
|
||||
>
|
||||
{data != undefined ? (
|
||||
getDisplayDate(data)
|
||||
) : (
|
||||
<Skeleton width="5rem" sx={{ mx: { xs: "auto", sm: "unset" } }} />
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ "& > *": { m: ".3rem !important" } }}>
|
||||
<Tooltip title={t("show.play")}>
|
||||
<NextLink href={data ? `/watch/${data.slug}` : ""} passHref>
|
||||
<Fab color="primary" size="small" aria-label={t("show.play")}>
|
||||
<PlayArrow />
|
||||
</Fab>
|
||||
</NextLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("show.trailer")} aria-label={t("show.trailer")}>
|
||||
<IconButton>
|
||||
<LocalMovies sx={{ color: { md: "white" } }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: { xs: "none", md: "flex" },
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: "25%",
|
||||
flexDirection: "column",
|
||||
alignSelf: "end",
|
||||
pr: "15px",
|
||||
}}
|
||||
>
|
||||
{data?.logo && (
|
||||
<Image
|
||||
img={data.logo}
|
||||
alt=""
|
||||
width="100%"
|
||||
height="100px"
|
||||
sx={{ display: { xs: "none", lg: "unset" } }}
|
||||
/>
|
||||
)}
|
||||
<StudioText loading={!data} studio={data?.studio} sx={{ mt: "auto", mb: 3 }} />
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
<Container sx={{ display: { xs: "block", sm: "none" }, pt: 3 }}>
|
||||
<StudioText loading={!data} studio={data?.studio} sx={{ mb: 1 }} />
|
||||
<Typography sx={{ mb: 1 }}>
|
||||
{t("show.genre")}
|
||||
{": "}
|
||||
{!data ? (
|
||||
<Skeleton width="10rem" sx={{ display: "inline-flex" }} />
|
||||
) : data?.genres && data.genres.length ? (
|
||||
data.genres.map((genre, i) => [
|
||||
i > 0 && ", ",
|
||||
<Link key={genre.id} href={`/genres/${genre.slug}`}>
|
||||
{genre.name}
|
||||
</Link>,
|
||||
])
|
||||
) : (
|
||||
t("show.genre-none")
|
||||
)}
|
||||
</Typography>
|
||||
</Container>
|
||||
|
||||
<Container sx={{ pt: 2 }}>
|
||||
<Typography align="justify" sx={{ flexBasis: 0, flexGrow: 1, pt: { sm: 2 } }}>
|
||||
{data
|
||||
? data.overview ?? t("show.noOverview")
|
||||
: [...Array(4)].map((_, i) => <Skeleton key={i} />)}
|
||||
</Typography>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
variant="middle"
|
||||
flexItem
|
||||
sx={{ mx: 2, display: { xs: "none", sm: "block" } }}
|
||||
/>
|
||||
<Box sx={{ flexBasis: "25%", display: { xs: "none", sm: "block" } }}>
|
||||
<StudioText
|
||||
loading={!data}
|
||||
studio={data?.studio}
|
||||
sx={{ display: { xs: "none", sm: "block", md: "none" }, pb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography variant="h4" component="h2">
|
||||
{t("show.genre")}
|
||||
</Typography>
|
||||
{!data || data.genres?.length ? (
|
||||
<ul>
|
||||
{(data ? data.genres! : [...Array(3)]).map((genre, i) => (
|
||||
<li key={genre?.id ?? i}>
|
||||
<Typography>
|
||||
{genre ? (
|
||||
<Link href={`/genres/${genre.slug}`}>{genre.name}</Link>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)}
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<Typography>{t("show.genre-none")}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShowStaff = ({ slug }: { slug: string }) => {
|
||||
const { items, isError, error } = useInfiniteFetch(ShowStaff.query(slug));
|
||||
const { t } = useTranslation("browse");
|
||||
|
||||
// TODO: handle infinite scroll
|
||||
|
||||
if (isError) return <ErrorComponent {...error} />;
|
||||
|
||||
return (
|
||||
<HorizontalList title={t("show.staff")} noContent={t("show.staff-none")}>
|
||||
{(items ?? [...Array(20)]).map((x, i) => (
|
||||
<PersonAvatar
|
||||
key={x ? x.id : i}
|
||||
person={x}
|
||||
sx={{ width: { xs: "7rem", lg: "10rem" }, flexShrink: 0, px: 2 }}
|
||||
/>
|
||||
))}
|
||||
</HorizontalList>
|
||||
);
|
||||
};
|
||||
|
||||
ShowStaff.query = (slug: string): QueryIdentifier<Person> => ({
|
||||
parser: PersonP,
|
||||
path: ["shows", slug, "people"],
|
||||
infinite: true,
|
||||
});
|
||||
|
||||
const query = (slug: string): QueryIdentifier<Movie> => ({
|
||||
parser: MovieP,
|
||||
path: ["shows", slug],
|
||||
params: {
|
||||
fields: ["genres", "studio"],
|
||||
},
|
||||
});
|
||||
|
||||
const MovieDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
const { data, error } = useFetch(query(slug));
|
||||
|
||||
if (error) return <ErrorPage {...error} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{makeTitle(data?.name)}</title>
|
||||
<meta name="description" content={data?.overview!} />
|
||||
</Head>
|
||||
<ShowHeader data={data} />
|
||||
<ShowStaff slug={slug} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MovieDetails.getFetchUrls = ({ slug }) => [query(slug), ShowStaff.query(slug), Navbar.query()];
|
||||
|
||||
export default withRoute(MovieDetails);
|
||||
|
44
front/packages/primitives/src/container.tsx
Normal file
44
front/packages/primitives/src/container.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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 { View, ViewProps } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
|
||||
export const Container = (props: ViewProps) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
display: "flex",
|
||||
paddingHorizontal: "15px",
|
||||
marginHorizontal: "auto",
|
||||
width: {
|
||||
sm: "540px",
|
||||
md: "880px",
|
||||
lg: "1170px",
|
||||
},
|
||||
},
|
||||
props,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -19,28 +19,69 @@
|
||||
*/
|
||||
|
||||
import MIcon from "@expo/vector-icons/MaterialIcons";
|
||||
import { ComponentProps } from "react";
|
||||
import { Pressable, useTheme } from "yoshiki/native";
|
||||
import { ComponentProps, ComponentType } from "react";
|
||||
import { PressableProps } from "react-native";
|
||||
import { Pressable, px, useYoshiki } from "yoshiki/native";
|
||||
import { Breakpoint, ts } from ".";
|
||||
|
||||
export type IconProps = {
|
||||
icon: ComponentProps<typeof MIcon>["name"];
|
||||
size?: number;
|
||||
color?: string;
|
||||
color?: Breakpoint<string>;
|
||||
};
|
||||
|
||||
export const Icon = ({ icon, size, color }: IconProps) => {
|
||||
return <MIcon name={icon} size={size ?? 24} color={color ?? "white"} />;
|
||||
export const Icon = ({ icon, size = 24, color }: IconProps) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
return (
|
||||
<MIcon
|
||||
name={icon}
|
||||
size={size}
|
||||
{...css({ color: color ?? theme.colors.white, width: size, height: size })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconButton = ({
|
||||
export const IconButton = <AsProps = PressableProps,>({
|
||||
icon,
|
||||
size,
|
||||
color,
|
||||
...props
|
||||
}: ComponentProps<typeof Pressable> & IconProps) => {
|
||||
as,
|
||||
...asProps
|
||||
}: IconProps & { as?: ComponentType<AsProps> } & AsProps) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
const Container = as ?? Pressable;
|
||||
|
||||
return (
|
||||
<Pressable {...props}>
|
||||
<Container
|
||||
{...(css(
|
||||
{
|
||||
p: ts(1),
|
||||
m: px(2),
|
||||
borderRadius: 9999,
|
||||
},
|
||||
asProps,
|
||||
) as AsProps)}
|
||||
>
|
||||
<Icon icon={icon} size={size} color={color} />
|
||||
</Pressable>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconFab = <AsProps = PressableProps,>(
|
||||
props: ComponentProps<typeof IconButton<AsProps>>,
|
||||
) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
colors={theme.colors.black}
|
||||
{...(css(
|
||||
{
|
||||
bg: (theme) => theme.accent,
|
||||
},
|
||||
props,
|
||||
) as any)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -132,8 +132,8 @@ export const ImageBackground = <AsProps = ViewProps,>({
|
||||
as?: ComponentType<AsProps>;
|
||||
gradient?: Partial<LinearGradientProps> | boolean;
|
||||
children: ReactNode;
|
||||
containerStyle?: StyleList<ViewStyle>;
|
||||
imageStyle?: StyleList<ImageStyle>;
|
||||
containerStyle?: YoshikiEnhanced<ViewStyle>;
|
||||
imageStyle?: YoshikiEnhanced<ImageStyle>;
|
||||
} & AsProps &
|
||||
Props) => {
|
||||
const [isErrored, setErrored] = useState(false);
|
||||
|
@ -18,7 +18,7 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export { Header, Nav, Footer } from "@expo/html-elements";
|
||||
export { Header, Main, Nav, Footer } from "@expo/html-elements";
|
||||
export * from "./text";
|
||||
export * from "./themes";
|
||||
export * from "./icons";
|
||||
@ -27,23 +27,15 @@ export * from "./avatar";
|
||||
export * from "./image";
|
||||
export * from "./skeleton";
|
||||
export * from "./tooltip";
|
||||
export * from "./container";
|
||||
|
||||
export * from "./animated";
|
||||
|
||||
export * from "./utils/breakpoints";
|
||||
export * from "./utils/nojs";
|
||||
|
||||
import { Dimensions } from "react-native";
|
||||
import { px } from "yoshiki/native";
|
||||
|
||||
export const ts = (spacing: number) => {
|
||||
return px(spacing * 8);
|
||||
};
|
||||
|
||||
export const vw = (spacing: number) => {
|
||||
return px(spacing * Dimensions.get('window').width / 100);
|
||||
};
|
||||
|
||||
export const vh = (spacing: number) => {
|
||||
return px(spacing * Dimensions.get('window').height / 100);
|
||||
};
|
||||
|
@ -69,7 +69,6 @@ export const Skeleton = ({
|
||||
borderRadius: px(6),
|
||||
},
|
||||
variant === "text" && {
|
||||
margin: rem(1),
|
||||
width: percent(75),
|
||||
height: rem(1.2),
|
||||
},
|
||||
|
@ -63,7 +63,7 @@ export const catppuccin: ThemeBuilder = {
|
||||
overlay1: "#9399b2",
|
||||
default: {
|
||||
background: "#1e1e2e",
|
||||
accent: "##f5c2e7",
|
||||
accent: "#f5c2e7",
|
||||
divider: "#7f849c",
|
||||
heading: "#cdd6f4",
|
||||
paragraph: "#bac2de",
|
||||
|
@ -23,6 +23,7 @@ import { Property } from "csstype";
|
||||
import { Theme, ThemeProvider } from "yoshiki";
|
||||
import { useTheme, useYoshiki } from "yoshiki/native";
|
||||
import "yoshiki";
|
||||
import "yoshiki/native";
|
||||
import { catppuccin } from "./catppuccin";
|
||||
|
||||
type ThemeSettings = {
|
||||
@ -57,11 +58,19 @@ type Variant = {
|
||||
};
|
||||
|
||||
declare module "yoshiki" {
|
||||
// TODO: Add specifics colors
|
||||
export interface Theme extends ThemeSettings, Mode, Variant {
|
||||
builder: ThemeBuilder;
|
||||
light: Mode & Variant;
|
||||
dark: Mode & Variant;
|
||||
user: Mode & Variant;
|
||||
}
|
||||
}
|
||||
// declare module "yoshiki/native" {
|
||||
// export interface Theme extends ThemeSettings, Mode, Variant {
|
||||
// light: Mode & Variant;
|
||||
// dark: Mode & Variant;
|
||||
// user: Mode & Variant;
|
||||
// }
|
||||
// }
|
||||
|
||||
export type { Theme } from "yoshiki";
|
||||
export type ThemeBuilder = ThemeSettings & {
|
||||
@ -69,14 +78,21 @@ export type ThemeBuilder = ThemeSettings & {
|
||||
dark: Mode & { default: Variant };
|
||||
};
|
||||
|
||||
export const selectMode = (theme: ThemeBuilder, mode: "light" | "dark"): Theme => {
|
||||
const { light, dark, ...options } = theme;
|
||||
const selectMode = (theme: ThemeBuilder, mode: "light" | "dark"): Theme => {
|
||||
const { light: lightBuilder, dark: darkBuilder, ...options } = theme;
|
||||
const light = { ...lightBuilder, ...lightBuilder.default };
|
||||
const dark = { ...darkBuilder, ...darkBuilder.default };
|
||||
const value = mode === "light" ? light : dark;
|
||||
const { default: def, ...modeOpt } = value;
|
||||
return { ...options, ...modeOpt, ...def, variant: value.variant, builder: theme };
|
||||
return {
|
||||
...options,
|
||||
...value,
|
||||
light,
|
||||
dark,
|
||||
user: value,
|
||||
};
|
||||
};
|
||||
|
||||
export const switchVariant = (theme: Theme) => {
|
||||
const switchVariant = (theme: Theme) => {
|
||||
return {
|
||||
...theme,
|
||||
...theme.variant,
|
||||
@ -122,7 +138,7 @@ export const ContrastArea = ({
|
||||
contrastText?: boolean;
|
||||
}) => {
|
||||
const oldTheme = useTheme();
|
||||
const theme = selectMode(oldTheme.builder, mode);
|
||||
const theme: Theme = { ...oldTheme, ...oldTheme[mode] };
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
|
@ -18,17 +18,21 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ToastAndroid, PressableProps } from "react-native";
|
||||
import { ToastAndroid, Platform } from "react-native";
|
||||
import { Theme } from "yoshiki/native";
|
||||
|
||||
export const tooltip = (tooltip: string) =>
|
||||
({
|
||||
dataSet: { tooltip },
|
||||
onLongPress: () => {
|
||||
// TODO handle IOS.
|
||||
ToastAndroid.show(tooltip, ToastAndroid.SHORT);
|
||||
Platform.select({
|
||||
web: {
|
||||
dataSet: { tooltip, label: tooltip },
|
||||
},
|
||||
} satisfies PressableProps);
|
||||
android: {
|
||||
onLongPress: () => {
|
||||
ToastAndroid.show(tooltip, ToastAndroid.SHORT);
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
});
|
||||
|
||||
export const WebTooltip = ({ theme }: { theme: Theme }) => {
|
||||
const background = `${theme.colors.black}CC`;
|
||||
@ -41,6 +45,7 @@ export const WebTooltip = ({ theme }: { theme: Theme }) => {
|
||||
|
||||
[data-tooltip]::after {
|
||||
content: attr(data-tooltip);
|
||||
display: flex;
|
||||
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
@ -55,6 +60,7 @@ export const WebTooltip = ({ theme }: { theme: Theme }) => {
|
||||
color: ${theme.colors.white};
|
||||
background-color: ${background};
|
||||
font-family: ${theme.fonts.paragraph};
|
||||
text-align: center;
|
||||
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
|
@ -18,6 +18,8 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export {}
|
||||
|
||||
// const SortByMenu = ({
|
||||
// sortKey,
|
||||
// setSort,
|
||||
|
308
front/packages/ui/src/details/header.tsx
Normal file
308
front/packages/ui/src/details/header.tsx
Normal file
@ -0,0 +1,308 @@
|
||||
/*
|
||||
* 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 { Movie, QueryIdentifier, Show, getDisplayDate } from "@kyoo/models";
|
||||
import {
|
||||
Container,
|
||||
H1,
|
||||
Main,
|
||||
ImageBackground,
|
||||
Skeleton,
|
||||
Poster,
|
||||
P,
|
||||
tooltip,
|
||||
Link,
|
||||
IconButton,
|
||||
IconFab,
|
||||
} from "@kyoo/primitives";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { em, percent, rem, vh, useYoshiki, Stylable } from "yoshiki/native";
|
||||
import { Fetch, WithLoading } from "../fetch";
|
||||
import { Navbar } from "../navbar";
|
||||
|
||||
// const StudioText = ({
|
||||
// studio,
|
||||
// loading = false,
|
||||
// sx,
|
||||
// }: {
|
||||
// studio?: Studio | null;
|
||||
// loading?: boolean;
|
||||
// sx?: SxProps;
|
||||
// }) => {
|
||||
// const { t } = useTranslation("browse");
|
||||
|
||||
// if (!loading && !studio) return null;
|
||||
// return (
|
||||
// <Typography sx={sx}>
|
||||
// {t("show.studio")}:{" "}
|
||||
// {loading ? (
|
||||
// <Skeleton width="5rem" sx={{ display: "inline-flex" }} />
|
||||
// ) : (
|
||||
// <Link href={`/studio/${studio!.slug}`}>{studio!.name}</Link>
|
||||
// )}
|
||||
// </Typography>
|
||||
// );
|
||||
// };
|
||||
|
||||
const TitleLine = ({
|
||||
isLoading,
|
||||
slug,
|
||||
name,
|
||||
date,
|
||||
poster,
|
||||
...props
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
slug: string;
|
||||
name?: string;
|
||||
date?: string;
|
||||
poster?: string | null;
|
||||
} & Stylable) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Container
|
||||
{...css(
|
||||
{
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
alignItems: { xs: "center", sm: "flex-start" },
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<Poster
|
||||
src={poster}
|
||||
alt={name}
|
||||
isLoading={isLoading}
|
||||
layout={{
|
||||
width: { xs: percent(50), md: percent(25) },
|
||||
}}
|
||||
{...css({ maxWidth: { xs: px(175), sm: "unset" }, flexShrink: 0 })}
|
||||
/>
|
||||
<View
|
||||
{...css({
|
||||
alignSelf: { xs: "center", sm: "flex-end", md: "center" },
|
||||
alignItems: { xs: "center", sm: "flex-start" },
|
||||
paddingLeft: { sm: em(2.5) },
|
||||
flexShrink: 1,
|
||||
})}
|
||||
>
|
||||
<Skeleton {...css({ width: rem(15), height: rem(3), marginBottom: rem(0.5) })}>
|
||||
{isLoading || (
|
||||
<H1
|
||||
{...css({
|
||||
fontWeight: { md: "900" },
|
||||
fontSize: rem(3),
|
||||
marginTop: 0,
|
||||
marginBottom: rem(0.5),
|
||||
textAlign: { xs: "center", sm: "flex-start" },
|
||||
color: (theme) => ({ xs: theme.user.heading, md: theme.heading }),
|
||||
})}
|
||||
>
|
||||
{name}
|
||||
</H1>
|
||||
)}
|
||||
</Skeleton>
|
||||
{(isLoading || date) && (
|
||||
<Skeleton
|
||||
{...css({
|
||||
width: rem(5),
|
||||
height: rem(1.5),
|
||||
marginBottom: rem(0.5),
|
||||
})}
|
||||
>
|
||||
{isLoading || (
|
||||
<P
|
||||
{...css({
|
||||
fontWeight: "300",
|
||||
fontSize: rem(1.5),
|
||||
letterSpacing: 0,
|
||||
marginTop: 0,
|
||||
marginBottom: rem(0.5),
|
||||
textAlign: { xs: "center", sm: "flex-start" },
|
||||
color: (theme) => ({ xs: theme.user.heading, md: theme.heading }),
|
||||
})}
|
||||
>
|
||||
{date}
|
||||
</P>
|
||||
)}
|
||||
</Skeleton>
|
||||
)}
|
||||
<View {...css({ flexDirection: "row" })} /*sx={{ "& > *": { m: ".3rem !important" } }} */>
|
||||
<IconFab
|
||||
icon="play-arrow"
|
||||
as={Link}
|
||||
href={`/watch/${slug}`}
|
||||
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
|
||||
{...css({ bg: { xs: theme.user.accent, md: theme.accent } })}
|
||||
{...tooltip(t("show.play"))}
|
||||
/>
|
||||
<IconButton
|
||||
icon="local-movies"
|
||||
color={{ xs: theme.user.colors.black, md: theme.colors.white }}
|
||||
{...tooltip(t("show.trailer"))}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{/* <View */}
|
||||
{/* {...css({ */}
|
||||
{/* display: { xs: "none", md: "flex" }, */}
|
||||
{/* flexDirection: "column", */}
|
||||
{/* alignSelf: "flex-end", */}
|
||||
{/* paddingRight: px(15), */}
|
||||
{/* })} */}
|
||||
{/* > */}
|
||||
{/* {(isLoading || logo || true) && ( */}
|
||||
{/* <Image */}
|
||||
{/* src={logo} */}
|
||||
{/* alt="" */}
|
||||
{/* layout={{ */}
|
||||
{/* width: "100%", */}
|
||||
{/* height: px(100), */}
|
||||
{/* }} */}
|
||||
{/* // sx={{ display: { xs: "none", lg: "unset" } }} */}
|
||||
{/* /> */}
|
||||
{/* )} */}
|
||||
{/* {/1* <StudioText loading={!data} studio={data?.studio} sx={{ mt: "auto", mb: 3 }} /> *1/} */}
|
||||
{/* </View> */}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
// const Tata = () => {
|
||||
// return (
|
||||
// <Container sx={{ pt: 2 }}>
|
||||
// <Typography align="justify" sx={{ flexBasis: 0, flexGrow: 1, pt: { sm: 2 } }}>
|
||||
// {data
|
||||
// ? data.overview ?? t("show.noOverview")
|
||||
// : [...Array(4)].map((_, i) => <Skeleton key={i} />)}
|
||||
// </Typography>
|
||||
// <Divider
|
||||
// orientation="vertical"
|
||||
// variant="middle"
|
||||
// flexItem
|
||||
// sx={{ mx: 2, display: { xs: "none", sm: "block" } }}
|
||||
// />
|
||||
// <Box sx={{ flexBasis: "25%", display: { xs: "none", sm: "block" } }}>
|
||||
// <StudioText
|
||||
// loading={!data}
|
||||
// studio={data?.studio}
|
||||
// sx={{ display: { xs: "none", sm: "block", md: "none" }, pb: 2 }}
|
||||
// />
|
||||
|
||||
// <Typography variant="h4" component="h2">
|
||||
// {t("show.genre")}
|
||||
// </Typography>
|
||||
// {!data || data.genres?.length ? (
|
||||
// <ul>
|
||||
// {(data ? data.genres! : [...Array(3)]).map((genre, i) => (
|
||||
// <li key={genre?.id ?? i}>
|
||||
// <Typography>
|
||||
// {genre ? <Link href={`/genres/${genre.slug}`}>{genre.name}</Link> : <Skeleton />}
|
||||
// </Typography>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// ) : (
|
||||
// <Typography>{t("show.genre-none")}</Typography>
|
||||
// )}
|
||||
// </Box>
|
||||
// </Container>
|
||||
// );
|
||||
// };
|
||||
|
||||
const min = Platform.OS === "web"
|
||||
? (...values: number[]): number => `min(${values.join(", ")})` as unknown as number
|
||||
: (...values: number[]): number => Math.min(...values);
|
||||
const max = Platform.OS === "web"
|
||||
? (...values: number[]): number => `max(${values.join(", ")})` as unknown as number
|
||||
: (...values: number[]): number => Math.max(...values);
|
||||
const px = Platform.OS === "web"
|
||||
? (value: number): number => `${value}px` as unknown as number
|
||||
: (value: number): number => value;
|
||||
|
||||
export const ShowHeader = ({
|
||||
query,
|
||||
slug,
|
||||
}: {
|
||||
query: QueryIdentifier<Show | Movie>;
|
||||
slug: string;
|
||||
}) => {
|
||||
/* const scroll = useScroll(); */
|
||||
const { css } = useYoshiki();
|
||||
// TODO: tweek the navbar color with the theme.
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar {...css({ bg: "transparent" })} />
|
||||
<Fetch query={query}>
|
||||
{({ isLoading, ...data }) => (
|
||||
<>
|
||||
{/* TODO: HEAD element for SEO*/}
|
||||
{/* TODO: Add a shadow on navbar items */}
|
||||
{/* TODO: Put the navbar outside of the scrollbox */}
|
||||
<ImageBackground
|
||||
src={data?.thumbnail}
|
||||
alt=""
|
||||
as={Main}
|
||||
containerStyle={{
|
||||
height: { xs: vh(40), sm: min(vh(60), px(750)), lg: vh(70) },
|
||||
minHeight: { xs: px(350), sm: px(500), lg: px(600) },
|
||||
}}
|
||||
{...css(StyleSheet.absoluteFillObject)}
|
||||
>
|
||||
<TitleLine
|
||||
isLoading={isLoading}
|
||||
slug={slug}
|
||||
name={data?.name}
|
||||
date={data ? getDisplayDate(data as any) : undefined}
|
||||
poster={data?.poster}
|
||||
{...css({
|
||||
marginTop: { xs: max(vh(20), px(200)), sm: vh(45), md: vh(35) }
|
||||
})}
|
||||
/>
|
||||
{/* <Container sx={{ display: { xs: "block", sm: "none" }, pt: 3 }}> */}
|
||||
{/* <StudioText loading={!data} studio={data?.studio} sx={{ mb: 1 }} /> */}
|
||||
{/* <Typography sx={{ mb: 1 }}> */}
|
||||
{/* {t("show.genre")} */}
|
||||
{/* {": "} */}
|
||||
{/* {!data ? ( */}
|
||||
{/* <Skeleton width="10rem" sx={{ display: "inline-flex" }} /> */}
|
||||
{/* ) : data?.genres && data.genres.length ? ( */}
|
||||
{/* data.genres.map((genre, i) => [ */}
|
||||
{/* i > 0 && ", ", */}
|
||||
{/* <Link key={genre.id} href={`/genres/${genre.slug}`}> */}
|
||||
{/* {genre.name} */}
|
||||
{/* </Link>, */}
|
||||
{/* ]) */}
|
||||
{/* ) : ( */}
|
||||
{/* t("show.genre-none") */}
|
||||
{/* )} */}
|
||||
{/* </Typography> */}
|
||||
{/* </Container> */}
|
||||
</ImageBackground>
|
||||
</>
|
||||
)}
|
||||
</Fetch>
|
||||
</>
|
||||
);
|
||||
};
|
47
front/packages/ui/src/details/index.tsx
Normal file
47
front/packages/ui/src/details/index.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 { Movie, MovieP, QueryIdentifier, QueryPage } from "@kyoo/models";
|
||||
import { ShowHeader } from "./header";
|
||||
|
||||
const query = (slug: string): QueryIdentifier<Movie> => ({
|
||||
parser: MovieP,
|
||||
path: ["shows", slug],
|
||||
params: {
|
||||
fields: ["genres", "studio"],
|
||||
},
|
||||
});
|
||||
|
||||
export const MovieDetails: QueryPage<{ slug: string }> = ({ slug }) => {
|
||||
return (
|
||||
<>
|
||||
{/* <Head> */}
|
||||
{/* <title>{makeTitle(data?.name)}</title> */}
|
||||
{/* <meta name="description" content={data?.overview!} /> */}
|
||||
{/* </Head> */}
|
||||
<ShowHeader slug={slug} query={query(slug)} />
|
||||
{/* <ShowStaff slug={slug} /> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MovieDetails.getFetchUrls = ({ slug }) => [//query(slug),
|
||||
// ShowStaff.query(slug), Navbar.query()
|
||||
];
|
47
front/packages/ui/src/details/staff.tsx
Normal file
47
front/packages/ui/src/details/staff.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export {}
|
||||
// export const ShowStaff = ({ slug }: { slug: string }) => {
|
||||
// const { items, isError, error } = useInfiniteFetch(ShowStaff.query(slug));
|
||||
// const { t } = useTranslation("browse");
|
||||
|
||||
// // TODO: handle infinite scroll
|
||||
|
||||
// if (isError) return <ErrorComponent {...error} />;
|
||||
|
||||
// return (
|
||||
// <HorizontalList title={t("show.staff")} noContent={t("show.staff-none")}>
|
||||
// {(items ?? [...Array(20)]).map((x, i) => (
|
||||
// <PersonAvatar
|
||||
// key={x ? x.id : i}
|
||||
// person={x}
|
||||
// sx={{ width: { xs: "7rem", lg: "10rem" }, flexShrink: 0, px: 2 }}
|
||||
// />
|
||||
// ))}
|
||||
// </HorizontalList>
|
||||
// );
|
||||
// };
|
||||
|
||||
// ShowStaff.query = (slug: string): QueryIdentifier<Person> => ({
|
||||
// parser: PersonP,
|
||||
// path: ["shows", slug, "people"],
|
||||
// infinite: true,
|
||||
// });
|
@ -34,11 +34,11 @@ const isPage = <T = unknown,>(obj: unknown): obj is Page<T> =>
|
||||
|
||||
export const Fetch = <Data,>({
|
||||
query,
|
||||
placeholderCount,
|
||||
placeholderCount = 1,
|
||||
children,
|
||||
}: {
|
||||
query: QueryIdentifier<Data>;
|
||||
placeholderCount: number;
|
||||
placeholderCount?: number;
|
||||
children: (
|
||||
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
|
||||
i: number,
|
||||
@ -52,7 +52,9 @@ export const Fetch = <Data,>({
|
||||
<>{[...Array(placeholderCount)].map((_, i) => children({ isLoading: true } as any, i))}</>
|
||||
);
|
||||
if (!isPage<object>(data))
|
||||
return children(data ? { ...data, isLoading: false } : ({ isLoading: true } as any), 0);
|
||||
return (
|
||||
<> {children(data ? { ...data, isLoading: false } : ({ isLoading: true } as any), 0)} </>
|
||||
);
|
||||
return <>{data.items.map((item, i) => children({ ...item, isLoading: false } as any, i))}</>;
|
||||
};
|
||||
|
||||
|
@ -20,3 +20,4 @@
|
||||
|
||||
export * from "./navbar";
|
||||
export { BrowsePage } from "./browse";
|
||||
export { MovieDetails } from "./details";
|
||||
|
@ -96,7 +96,7 @@ export const Navbar = (props: Stylable) => {
|
||||
{library.name}
|
||||
</A>
|
||||
) : (
|
||||
<Skeleton key={i} {...css({ width: rem(5) })} />
|
||||
<Skeleton key={i} {...css({ width: rem(5), marginX: ts(1) })} />
|
||||
)
|
||||
}
|
||||
</Fetch>
|
||||
|
@ -9682,7 +9682,7 @@ __metadata:
|
||||
react-native-screens: ~3.18.0
|
||||
react-native-svg: 13.4.0
|
||||
typescript: ^4.6.3
|
||||
yoshiki: 0.2.9
|
||||
yoshiki: 0.2.11
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -13319,7 +13319,7 @@ __metadata:
|
||||
superjson: ^1.11.0
|
||||
typescript: ^4.9.3
|
||||
webpack: ^5.75.0
|
||||
yoshiki: 0.2.9
|
||||
yoshiki: 0.2.11
|
||||
zod: ^3.19.1
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@ -13644,9 +13644,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yoshiki@npm:0.2.9":
|
||||
version: 0.2.9
|
||||
resolution: "yoshiki@npm:0.2.9"
|
||||
"yoshiki@npm:0.2.11":
|
||||
version: 0.2.11
|
||||
resolution: "yoshiki@npm:0.2.11"
|
||||
dependencies:
|
||||
"@types/node": 18.x.x
|
||||
"@types/react": 18.x.x
|
||||
@ -13661,7 +13661,7 @@ __metadata:
|
||||
optional: true
|
||||
react-native-web:
|
||||
optional: true
|
||||
checksum: 41ff5ff7e4cd99b2bc7453749a1f17ceb63bc9e1123285bc62a12315cac1fb087b58366db50e08b275e7dd0b1ce862df4c3acae51872f8e3aa8765ee9b61d4bf
|
||||
checksum: 5a2bbb62b2270d3456f114cfbb24a84ad6b8a94b147687929ccffe2d179560ef40b46df1d4054eda91310d295a9a674bbb201765deb86dc96a2133bfd702235a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user