mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-03 19:17:16 -05: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