mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-01 20:54:13 -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/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Navbar } from "@kyoo/ui";
|
import BrowsePage from "./browse";
|
||||||
import { Text, View } from "react-native";
|
|
||||||
import { useYoshiki } from "yoshiki/native";
|
|
||||||
|
|
||||||
const App = () => {
|
// While there is no home page, show the browse page.
|
||||||
const { css } = useYoshiki();
|
export default BrowsePage;
|
||||||
|
|
||||||
return (
|
|
||||||
<View {...css({ backgroundColor: (theme) => theme.background })}>
|
|
||||||
<Navbar />
|
|
||||||
<Text>toto</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
@ -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 appProps = await NextApp.getInitialProps(ctx);
|
||||||
|
|
||||||
const getUrl = (ctx.Component as QueryPage).getFetchUrls;
|
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);
|
appProps.pageProps.queryState = await fetchQuery(urls);
|
||||||
|
|
||||||
return { pageProps: superjson.serialize(appProps.pageProps) };
|
return { pageProps: superjson.serialize(appProps.pageProps) };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default withTranslations(App);
|
export default withTranslations(App);
|
||||||
|
@ -18,422 +18,427 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FilterList, GridView, North, Sort, South, ViewList } from "@mui/icons-material";
|
import { BrowsePage } from "@kyoo/ui";
|
||||||
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 { 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);
|
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 { ComponentProps } from "react";
|
||||||
import { useYoshiki, rem, px, Stylable } from "yoshiki/native";
|
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();
|
const { css } = useYoshiki();
|
||||||
return (
|
return (
|
||||||
<View {...css({ margin: px(2) }, { style })}>
|
<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>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -31,17 +31,23 @@ import {
|
|||||||
P as EP,
|
P as EP,
|
||||||
} from "@expo/html-elements";
|
} 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 Text = (props: ComponentProps<typeof EP>) => {
|
||||||
const { css, theme } = useYoshiki();
|
const { css, theme } = useYoshiki();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component
|
<Component
|
||||||
{...css(
|
{...css(
|
||||||
|
[
|
||||||
{
|
{
|
||||||
fontFamily: heading ? theme.fonts.heading : theme.fonts.paragraph,
|
fontFamily: type === "header" ? theme.fonts.heading : theme.fonts.paragraph,
|
||||||
color: heading ? theme.heading : theme.paragraph,
|
color: type === "header" ? theme.heading : theme.paragraph,
|
||||||
},
|
},
|
||||||
|
type === "sub" && { fontWeight: "300" },
|
||||||
|
],
|
||||||
props as TextProps,
|
props as TextProps,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -50,10 +56,11 @@ const styleText = (Component: ComponentType<ComponentProps<typeof EP>>, heading?
|
|||||||
return Text;
|
return Text;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const H1 = styleText(EH1, true);
|
export const H1 = styleText(EH1, "header");
|
||||||
export const H2 = styleText(EH2, true);
|
export const H2 = styleText(EH2, "header");
|
||||||
export const H3 = styleText(EH3, true);
|
export const H3 = styleText(EH3, "header");
|
||||||
export const H4 = styleText(EH4, true);
|
export const H4 = styleText(EH4, "header");
|
||||||
export const H5 = styleText(EH5, true);
|
export const H5 = styleText(EH5, "header");
|
||||||
export const H6 = styleText(EH6, true);
|
export const H6 = styleText(EH6, "header");
|
||||||
export const P = styleText(EP);
|
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/>.
|
* 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 { P } from "@kyoo/primitives";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/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> =>
|
const isPage = <T = unknown,>(obj: unknown): obj is Page<T> =>
|
||||||
(typeof obj === "object" && obj && "items" in obj) || false;
|
(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))}</>;
|
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 }) => {
|
export const ErrorView = ({ error }: { error: KyooErrors }) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
@ -60,7 +85,8 @@ export const ErrorView = ({ error }: { error: KyooErrors }) => {
|
|||||||
{...css({
|
{...css({
|
||||||
backgroundColor: (theme) => theme.colors.red,
|
backgroundColor: (theme) => theme.colors.red,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: "center"
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{error.errors.map((x, i) => (
|
{error.errors.map((x, i) => (
|
||||||
|
@ -19,3 +19,4 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./navbar";
|
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