mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Rewrite the browse page (part 1)
This commit is contained in:
parent
1f049952cc
commit
43ed65bc76
3
front/apps/mobile/app/browse/index.tsx
Normal file
3
front/apps/mobile/app/browse/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import { BrowsePage } from "@kyoo/ui";
|
||||
|
||||
export default BrowsePage
|
@ -18,19 +18,7 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Navbar } from "@kyoo/ui";
|
||||
import { Text, View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import BrowsePage from "./browse";
|
||||
|
||||
const App = () => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View {...css({ backgroundColor: (theme) => theme.background })}>
|
||||
<Navbar />
|
||||
<Text>toto</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
// While there is no home page, show the browse page.
|
||||
export default BrowsePage;
|
||||
|
@ -1,33 +0,0 @@
|
||||
{
|
||||
"show": {
|
||||
"play": "Play",
|
||||
"trailer": "Play Trailer",
|
||||
"studio": "Studio",
|
||||
"genre": "Genres",
|
||||
"genre-none": "No genres",
|
||||
"staff": "Staff",
|
||||
"staff-none": "The staff is unknown",
|
||||
"noOverview": "No overview available",
|
||||
"episode-none": "There is no episodes in this season",
|
||||
"episodeNoMetadata": "No metadata available"
|
||||
},
|
||||
"browse": {
|
||||
"sortby": "Sort by {{key}}",
|
||||
"sortby-tt": "Sort by",
|
||||
"sortkey": {
|
||||
"name": "Name",
|
||||
"startAir": "Start air",
|
||||
"endAir": "End air"
|
||||
},
|
||||
"sortord": {
|
||||
"asc": "asc",
|
||||
"desc": "decs"
|
||||
},
|
||||
"switchToGrid": "Switch to grid view",
|
||||
"switchToList": "Switch to list view"
|
||||
},
|
||||
"misc": {
|
||||
"prev-page": "Previous page",
|
||||
"next-page": "Next page"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"navbar": {
|
||||
"home": "Home",
|
||||
"login": "Login"
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"back": "Back",
|
||||
"previous": "Previous episode",
|
||||
"next": "Next episode",
|
||||
"play": "Play",
|
||||
"pause": "Pause",
|
||||
"mute": "Toggle mute",
|
||||
"volume": "Volume",
|
||||
"subtitles": "Subtitles",
|
||||
"subtitle-none": "None",
|
||||
"fullscreen": "Fullscreen"
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
{
|
||||
"show": {
|
||||
"play": "Lecture",
|
||||
"trailer": "Jouer le trailer",
|
||||
"studio": "Studio",
|
||||
"genre": "Genres",
|
||||
"genre-none": "Aucun genres",
|
||||
"staff": "Staff",
|
||||
"staff-none": "Aucun membre du staff connu",
|
||||
"noOverview": "Aucune description disponible",
|
||||
"episode-none": "Il n'y a pas d'épisodes dans cette saison",
|
||||
"episodeNoMetadata": "Aucune metadonnée disponible"
|
||||
},
|
||||
"browse": {
|
||||
"sortby": "Trier par {{key}}",
|
||||
"sortby-tt": "Trier par",
|
||||
"sortkey": {
|
||||
"name": "Nom",
|
||||
"startAir": "Date de sortie",
|
||||
"endAir": "Date de fin de sortie"
|
||||
},
|
||||
"sortord": {
|
||||
"asc": "asc",
|
||||
"desc": "decs"
|
||||
},
|
||||
"switchToGrid": "Passer en vue grille",
|
||||
"switchToList": "Passer en vue liste"
|
||||
},
|
||||
"misc": {
|
||||
"prev-page": "Page précédente",
|
||||
"next-page": "Page suivante"
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"navbar": {
|
||||
"home": "Accueil",
|
||||
"login": "Connexion"
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"back": "Retour",
|
||||
"previous": "Episode précédent",
|
||||
"next": "Episode suivant",
|
||||
"play": "Jouer",
|
||||
"pause": "Pause",
|
||||
"mute": "Muet",
|
||||
"volume": "Volume",
|
||||
"subtitles": "Sous titres",
|
||||
"subtitle-none": "Aucun",
|
||||
"fullscreen": "Plein-écran"
|
||||
}
|
@ -100,11 +100,15 @@ App.getInitialProps = async (ctx: AppContext) => {
|
||||
const appProps = await NextApp.getInitialProps(ctx);
|
||||
|
||||
const getUrl = (ctx.Component as QueryPage).getFetchUrls;
|
||||
const urls: QueryIdentifier[] = getUrl ? getUrl(ctx.router.query as any) : [];
|
||||
const getLayoutUrl = ((ctx.Component as QueryPage).getLayout as QueryPage)?.getFetchUrls;
|
||||
|
||||
const urls: QueryIdentifier[] = [
|
||||
...(getUrl ? getUrl(ctx.router.query as any) : []),
|
||||
...(getLayoutUrl ? getLayoutUrl(ctx.router.query as any) : []),
|
||||
];
|
||||
appProps.pageProps.queryState = await fetchQuery(urls);
|
||||
|
||||
return { pageProps: superjson.serialize(appProps.pageProps) };
|
||||
};
|
||||
|
||||
|
||||
export default withTranslations(App);
|
||||
|
@ -18,422 +18,427 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 { BrowsePage } from "@kyoo/ui";
|
||||
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);
|
||||
|
||||
/* 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); */
|
||||
|
@ -23,11 +23,19 @@ import { Skeleton as MSkeleton } from "moti/skeleton";
|
||||
import { ComponentProps } from "react";
|
||||
import { useYoshiki, rem, px, Stylable } from "yoshiki/native";
|
||||
|
||||
export const Skeleton = ({ style, ...props }: ComponentProps<typeof MSkeleton> & Stylable) => {
|
||||
export const Skeleton = ({
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof MSkeleton>, "children"> & {
|
||||
children: ComponentProps<typeof MSkeleton>["children"] | boolean;
|
||||
} & Stylable) => {
|
||||
const { css } = useYoshiki();
|
||||
return (
|
||||
<View {...css({ margin: px(2) }, { style })}>
|
||||
<MSkeleton colorMode="light" radius={6} height={rem(1.2)} {...props} />
|
||||
<MSkeleton colorMode="light" radius={6} height={rem(1.2)} {...props}>
|
||||
{children !== true ? children || undefined : undefined}
|
||||
</MSkeleton>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
@ -31,17 +31,23 @@ import {
|
||||
P as EP,
|
||||
} from "@expo/html-elements";
|
||||
|
||||
const styleText = (Component: ComponentType<ComponentProps<typeof EP>>, heading?: boolean) => {
|
||||
const styleText = (
|
||||
Component: ComponentType<ComponentProps<typeof EP>>,
|
||||
type?: "header" | "sub",
|
||||
) => {
|
||||
const Text = (props: ComponentProps<typeof EP>) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...css(
|
||||
{
|
||||
fontFamily: heading ? theme.fonts.heading : theme.fonts.paragraph,
|
||||
color: heading ? theme.heading : theme.paragraph,
|
||||
},
|
||||
[
|
||||
{
|
||||
fontFamily: type === "header" ? theme.fonts.heading : theme.fonts.paragraph,
|
||||
color: type === "header" ? theme.heading : theme.paragraph,
|
||||
},
|
||||
type === "sub" && { fontWeight: "300" },
|
||||
],
|
||||
props as TextProps,
|
||||
)}
|
||||
/>
|
||||
@ -50,10 +56,11 @@ const styleText = (Component: ComponentType<ComponentProps<typeof EP>>, heading?
|
||||
return Text;
|
||||
};
|
||||
|
||||
export const H1 = styleText(EH1, true);
|
||||
export const H2 = styleText(EH2, true);
|
||||
export const H3 = styleText(EH3, true);
|
||||
export const H4 = styleText(EH4, true);
|
||||
export const H5 = styleText(EH5, true);
|
||||
export const H6 = styleText(EH6, true);
|
||||
export const H1 = styleText(EH1, "header");
|
||||
export const H2 = styleText(EH2, "header");
|
||||
export const H3 = styleText(EH3, "header");
|
||||
export const H4 = styleText(EH4, "header");
|
||||
export const H5 = styleText(EH5, "header");
|
||||
export const H6 = styleText(EH6, "header");
|
||||
export const P = styleText(EP);
|
||||
export const SubP = styleText(EP, "sub");
|
||||
|
64
front/packages/ui/src/browse/grid.tsx
Normal file
64
front/packages/ui/src/browse/grid.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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 { A, Skeleton, Poster, ts, P, SubP } from "@kyoo/primitives";
|
||||
import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
|
||||
import { WithLoading } from "../fetch";
|
||||
|
||||
export const ItemGrid = ({
|
||||
href,
|
||||
name,
|
||||
subtitle,
|
||||
poster,
|
||||
isLoading,
|
||||
...props
|
||||
}: WithLoading<{
|
||||
href: string;
|
||||
name: string;
|
||||
subtitle?: string;
|
||||
poster?: string | null;
|
||||
}> &
|
||||
Stylable<"text">) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<A
|
||||
href={href ?? ""}
|
||||
{...css(
|
||||
{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
width: { xs: percent(18), sm: percent(25) },
|
||||
minWidth: { xs: px(90), sm: px(120) },
|
||||
maxWidth: px(168),
|
||||
m: { xs: ts(1), sm: ts(2) },
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<Poster src={poster} alt={name} width={percent(100)} />
|
||||
<Skeleton width={percent(80)}>{isLoading || <P>{name}</P>}</Skeleton>
|
||||
{(isLoading || subtitle) && (
|
||||
<Skeleton width={percent(50)}>{isLoading || <SubP>{subtitle}</SubP>}</Skeleton>
|
||||
)}
|
||||
</A>
|
||||
);
|
||||
};
|
103
front/packages/ui/src/browse/index.tsx
Normal file
103
front/packages/ui/src/browse/index.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 { ComponentProps, useState } from "react";
|
||||
import {
|
||||
QueryIdentifier,
|
||||
QueryPage,
|
||||
LibraryItem,
|
||||
LibraryItemP,
|
||||
ItemType,
|
||||
getDisplayDate,
|
||||
} from "@kyoo/models";
|
||||
import { DefaultLayout } from "../layout";
|
||||
import { InfiniteFetch, WithLoading } from "../fetch";
|
||||
import { ItemGrid } from "./grid";
|
||||
import { SortBy, SortOrd, Layout } from "./types";
|
||||
|
||||
const itemMap = (item: WithLoading<LibraryItem>): WithLoading<ComponentProps<typeof ItemGrid>> => {
|
||||
if (item.isLoading) return item;
|
||||
|
||||
let href;
|
||||
if (item?.type === ItemType.Movie) href = `/movie/${item.slug}`;
|
||||
else if (item?.type === ItemType.Show) href = `/show/${item.slug}`;
|
||||
else href = `/collection/${item.slug}`;
|
||||
|
||||
return {
|
||||
isLoading: item.isLoading,
|
||||
name: item.name,
|
||||
subtitle: item.type !== ItemType.Collection ? getDisplayDate(item) : undefined,
|
||||
href,
|
||||
poster: item.poster,
|
||||
};
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
||||
export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
|
||||
const [sortKey, setSort] = useState(SortBy.Name);
|
||||
const [sortOrd, setSortOrd] = useState(SortOrd.Asc);
|
||||
const [layout, setLayout] = useState(Layout.Grid);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <BrowseSettings */}
|
||||
{/* sortKey={sortKey} */}
|
||||
{/* setSort={setSort} */}
|
||||
{/* sortOrd={sortOrd} */}
|
||||
{/* setSortOrd={setSortOrd} */}
|
||||
{/* layout={layout} */}
|
||||
{/* setLayout={setLayout} */}
|
||||
{/* /> */}
|
||||
<InfiniteFetch
|
||||
query={query(slug, sortKey, sortOrd)}
|
||||
placeholderCount={15}
|
||||
/* sx={{ */
|
||||
/* display: "flex", */
|
||||
/* flexWrap: "wrap", */
|
||||
/* alignItems: "flex-start", */
|
||||
/* justifyContent: "center", */
|
||||
/* }} */
|
||||
>
|
||||
{(item, i) => <ItemGrid key={item?.id ?? i} {...itemMap(item)} />}
|
||||
</InfiniteFetch>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BrowsePage.getLayout = DefaultLayout;
|
||||
|
||||
BrowsePage.getFetchUrls = ({ slug, sortBy }) => [
|
||||
query(slug, sortBy?.split("-")[0] as SortBy, sortBy?.split("-")[1] as SortOrd),
|
||||
];
|
115
front/packages/ui/src/browse/list.tsx
Normal file
115
front/packages/ui/src/browse/list.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
188
front/packages/ui/src/browse/toto.tsx
Normal file
188
front/packages/ui/src/browse/toto.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
35
front/packages/ui/src/browse/types.ts
Normal file
35
front/packages/ui/src/browse/types.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 enum SortBy {
|
||||
Name = "name",
|
||||
StartAir = "startAir",
|
||||
EndAir = "endAir",
|
||||
}
|
||||
|
||||
export enum SortOrd {
|
||||
Asc = "asc",
|
||||
Desc = "desc",
|
||||
}
|
||||
|
||||
export enum Layout {
|
||||
Grid,
|
||||
List,
|
||||
}
|
@ -18,12 +18,14 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Page, QueryIdentifier, useFetch, KyooErrors } from "@kyoo/models";
|
||||
import { Page, QueryIdentifier, useFetch, KyooErrors, useInfiniteFetch } from "@kyoo/models";
|
||||
import { P } from "@kyoo/primitives";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
|
||||
export type WithLoading<Item> = (Item & { isLoading: false }) | { isLoading: true };
|
||||
export type WithLoading<Item> =
|
||||
| (Item & { isLoading: false })
|
||||
| (Partial<Item> & { isLoading: true });
|
||||
|
||||
const isPage = <T = unknown,>(obj: unknown): obj is Page<T> =>
|
||||
(typeof obj === "object" && obj && "items" in obj) || false;
|
||||
@ -52,6 +54,29 @@ export const Fetch = <Data,>({
|
||||
return <>{data.items.map((item, i) => children({ ...item, isLoading: false } as any, i))}</>;
|
||||
};
|
||||
|
||||
export const InfiniteFetch = <Data,>({
|
||||
query,
|
||||
placeholderCount = 15,
|
||||
children,
|
||||
}: {
|
||||
query: QueryIdentifier<Data>;
|
||||
placeholderCount?: number;
|
||||
children: (
|
||||
item: Data extends Page<infer Item> ? WithLoading<Item> : WithLoading<Data>,
|
||||
i: number,
|
||||
) => JSX.Element | null;
|
||||
}): JSX.Element | null => {
|
||||
if (!query.infinite) console.warn("A non infinite query was passed to an InfiniteFetch.");
|
||||
const { items, error } = useInfiniteFetch(query);
|
||||
|
||||
if (error) return <ErrorView error={error} />;
|
||||
if (!items)
|
||||
return (
|
||||
<>{[...Array(placeholderCount)].map((_, i) => children({ isLoading: true } as any, i))}</>
|
||||
);
|
||||
return <>{items.map((item, i) => children({ ...item, isLoading: false } as any, i))}</>;
|
||||
};
|
||||
|
||||
export const ErrorView = ({ error }: { error: KyooErrors }) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
@ -60,7 +85,8 @@ export const ErrorView = ({ error }: { error: KyooErrors }) => {
|
||||
{...css({
|
||||
backgroundColor: (theme) => theme.colors.red,
|
||||
flex: 1,
|
||||
alignItems: "center"
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
{error.errors.map((x, i) => (
|
||||
|
@ -19,3 +19,4 @@
|
||||
*/
|
||||
|
||||
export * from "./navbar";
|
||||
export { BrowsePage } from "./browse";
|
||||
|
36
front/packages/ui/src/layout.tsx
Normal file
36
front/packages/ui/src/layout.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 { ReactElement } from "react";
|
||||
import { Navbar } from "./navbar";
|
||||
import { useYoshiki } from "yoshiki";
|
||||
|
||||
export const DefaultLayout = (page: ReactElement) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main {...css({ flex: 1, display: "flex" })}>{page}</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DefaultLayout.query = () => [Navbar.query()];
|
Loading…
x
Reference in New Issue
Block a user