diff --git a/front/apps/mobile/app/browse/index.tsx b/front/apps/mobile/app/browse/index.tsx new file mode 100644 index 00000000..1815e516 --- /dev/null +++ b/front/apps/mobile/app/browse/index.tsx @@ -0,0 +1,3 @@ +import { BrowsePage } from "@kyoo/ui"; + +export default BrowsePage diff --git a/front/apps/mobile/app/index.tsx b/front/apps/mobile/app/index.tsx index 9447d5b2..420d6e22 100644 --- a/front/apps/mobile/app/index.tsx +++ b/front/apps/mobile/app/index.tsx @@ -18,19 +18,7 @@ * along with Kyoo. If not, see . */ -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 ( - theme.background })}> - - toto - - ); -}; - -export default App; +// While there is no home page, show the browse page. +export default BrowsePage; diff --git a/front/apps/web/locales/en/browse.json b/front/apps/web/locales/en/browse.json deleted file mode 100644 index 2b365d6f..00000000 --- a/front/apps/web/locales/en/browse.json +++ /dev/null @@ -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" - } -} diff --git a/front/apps/web/locales/en/common.json b/front/apps/web/locales/en/common.json deleted file mode 100644 index 4f619020..00000000 --- a/front/apps/web/locales/en/common.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "navbar": { - "home": "Home", - "login": "Login" - } -} diff --git a/front/apps/web/locales/en/player.json b/front/apps/web/locales/en/player.json deleted file mode 100644 index ecd1e99a..00000000 --- a/front/apps/web/locales/en/player.json +++ /dev/null @@ -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" -} diff --git a/front/apps/web/locales/fr/browse.json b/front/apps/web/locales/fr/browse.json deleted file mode 100644 index cbe5eeec..00000000 --- a/front/apps/web/locales/fr/browse.json +++ /dev/null @@ -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" - } -} diff --git a/front/apps/web/locales/fr/common.json b/front/apps/web/locales/fr/common.json deleted file mode 100644 index a675e263..00000000 --- a/front/apps/web/locales/fr/common.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "navbar": { - "home": "Accueil", - "login": "Connexion" - } -} diff --git a/front/apps/web/locales/fr/player.json b/front/apps/web/locales/fr/player.json deleted file mode 100644 index d5d721e7..00000000 --- a/front/apps/web/locales/fr/player.json +++ /dev/null @@ -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" -} diff --git a/front/apps/web/src/pages/_app.tsx b/front/apps/web/src/pages/_app.tsx index f9196fda..701b656b 100755 --- a/front/apps/web/src/pages/_app.tsx +++ b/front/apps/web/src/pages/_app.tsx @@ -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); diff --git a/front/apps/web/src/pages/browse/index.tsx b/front/apps/web/src/pages/browse/index.tsx index 64fc8e46..98cae566 100644 --- a/front/apps/web/src/pages/browse/index.tsx +++ b/front/apps/web/src/pages/browse/index.tsx @@ -18,422 +18,427 @@ * along with Kyoo. If not, see . */ -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 ( - - - {name ?? } - {(loading || subtitle) && ( - - {subtitle ?? } - - )} - - ); -}; - -const ItemList = ({ - href, - name, - subtitle, - thumbnail, - poster, - loading, -}: { - href?: string; - name?: string; - subtitle?: string | null; - poster?: string | null; - thumbnail?: string | null; - loading?: boolean; -}) => { - return ( - - {name} - - - {name ?? } - - {(loading || subtitle) && ( - - {subtitle ?? } - - )} - - - - ); -}; - -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 ( - - ); - case Layout.List: - return ( - - ); - } -}; - -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 ( - - {Object.values(SortBy).map((x) => ( - setSort(x)} - component={Link} - to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} - shallow - replace - > - {t(`browse.sortkey.${x}`)} - - ))} - - setSortOrd(SortOrd.Asc)} - component={Link} - to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} - shallow - replace - > - - - - {t("browse.sortord.asc")} - - setSortOrd(SortOrd.Desc)} - component={Link} - to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} - shallow - replace - > - - - - {t("browse.sortord.desc")} - - - ); -}; - -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(null); - const { t } = useTranslation("browse"); - - const switchViewTitle = - layout === Layout.Grid ? t("browse.switchToList") : t("browse.switchToGrid"); - - return ( - <> - - - - - - - - - - - - {sortAnchor && ( - setSortAnchor(null)} - /> - )} - - ); -}; - -const query = ( - slug?: string, - sortKey?: SortBy, - sortOrd?: SortOrd, -): QueryIdentifier => ({ - 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 ; - - return ( - <> - - )]} - sx={{ - display: "flex", - flexWrap: "wrap", - alignItems: "flex-start", - justifyContent: "center", - }} - > - {(items ?? [...Array(12)]).map((x, i) => ( - - ))} - - - ); -}; - -BrowsePage.getLayout = (page) => { - return ( - <> - -
{page}
- - ); -}; - -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 ( */ +/* */ +/* */ +/* {name ?? } */ +/* {(loading || subtitle) && ( */ +/* */ +/* {subtitle ?? } */ +/* */ +/* )} */ +/* */ +/* ); */ +/* }; */ + +/* const ItemList = ({ */ +/* href, */ +/* name, */ +/* subtitle, */ +/* thumbnail, */ +/* poster, */ +/* loading, */ +/* }: { */ +/* href?: string; */ +/* name?: string; */ +/* subtitle?: string | null; */ +/* poster?: string | null; */ +/* thumbnail?: string | null; */ +/* loading?: boolean; */ +/* }) => { */ +/* return ( */ +/* */ +/* {name} */ +/* */ +/* */ +/* {name ?? } */ +/* */ +/* {(loading || subtitle) && ( */ +/* */ +/* {subtitle ?? } */ +/* */ +/* )} */ +/* */ +/* */ +/* */ +/* ); */ +/* }; */ + +/* 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 ( */ +/* */ +/* ); */ +/* case Layout.List: */ +/* return ( */ +/* */ +/* ); */ +/* } */ +/* }; */ + +/* 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 ( */ +/* */ +/* {Object.values(SortBy).map((x) => ( */ +/* setSort(x)} */ +/* component={Link} */ +/* to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} */ +/* shallow */ +/* replace */ +/* > */ +/* {t(`browse.sortkey.${x}`)} */ +/* */ +/* ))} */ +/* */ +/* setSortOrd(SortOrd.Asc)} */ +/* component={Link} */ +/* to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} */ +/* shallow */ +/* replace */ +/* > */ +/* */ +/* */ +/* */ +/* {t("browse.sortord.asc")} */ +/* */ +/* setSortOrd(SortOrd.Desc)} */ +/* component={Link} */ +/* to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} */ +/* shallow */ +/* replace */ +/* > */ +/* */ +/* */ +/* */ +/* {t("browse.sortord.desc")} */ +/* */ +/* */ +/* ); */ +/* }; */ + +/* 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(null); */ +/* const { t } = useTranslation("browse"); */ + +/* const switchViewTitle = */ +/* layout === Layout.Grid ? t("browse.switchToList") : t("browse.switchToGrid"); */ + +/* return ( */ +/* <> */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* */ +/* {sortAnchor && ( */ +/* setSortAnchor(null)} */ +/* /> */ +/* )} */ +/* */ +/* ); */ +/* }; */ + +/* const query = ( */ +/* slug?: string, */ +/* sortKey?: SortBy, */ +/* sortOrd?: SortOrd, */ +/* ): QueryIdentifier => ({ */ +/* 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 ; */ + +/* return ( */ +/* <> */ +/* */ +/* )]} */ +/* sx={{ */ +/* display: "flex", */ +/* flexWrap: "wrap", */ +/* alignItems: "flex-start", */ +/* justifyContent: "center", */ +/* }} */ +/* > */ +/* {(items ?? [...Array(12)]).map((x, i) => ( */ +/* */ +/* ))} */ +/* */ +/* */ +/* ); */ +/* }; */ + +/* BrowsePage.getLayout = (page) => { */ +/* return ( */ +/* <> */ +/* */ +/*
{page}
*/ +/* */ +/* ); */ +/* }; */ + +/* BrowsePage.getFetchUrls = ({ slug, sortBy }) => [ */ +/* query(slug, sortBy?.split("-")[0] as SortBy, sortBy?.split("-")[1] as SortOrd), */ +/* Navbar.query(), */ +/* ]; */ + +/* export default withRoute(BrowsePage); */ diff --git a/front/packages/primitives/src/skeleton.tsx b/front/packages/primitives/src/skeleton.tsx index acefffa2..8d9ec076 100644 --- a/front/packages/primitives/src/skeleton.tsx +++ b/front/packages/primitives/src/skeleton.tsx @@ -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 & Stylable) => { +export const Skeleton = ({ + style, + children, + ...props +}: Omit, "children"> & { + children: ComponentProps["children"] | boolean; +} & Stylable) => { const { css } = useYoshiki(); return ( - + + {children !== true ? children || undefined : undefined} + ); }; diff --git a/front/packages/primitives/src/text.tsx b/front/packages/primitives/src/text.tsx index 8814c195..f3f279d1 100644 --- a/front/packages/primitives/src/text.tsx +++ b/front/packages/primitives/src/text.tsx @@ -31,17 +31,23 @@ import { P as EP, } from "@expo/html-elements"; -const styleText = (Component: ComponentType>, heading?: boolean) => { +const styleText = ( + Component: ComponentType>, + type?: "header" | "sub", +) => { const Text = (props: ComponentProps) => { const { css, theme } = useYoshiki(); return ( @@ -50,10 +56,11 @@ const styleText = (Component: ComponentType>, 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"); diff --git a/front/packages/ui/src/browse/grid.tsx b/front/packages/ui/src/browse/grid.tsx new file mode 100644 index 00000000..95292472 --- /dev/null +++ b/front/packages/ui/src/browse/grid.tsx @@ -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 . + */ + +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 ( + + + {isLoading ||

{name}

}
+ {(isLoading || subtitle) && ( + {isLoading || {subtitle}} + )} +
+ ); +}; diff --git a/front/packages/ui/src/browse/index.tsx b/front/packages/ui/src/browse/index.tsx new file mode 100644 index 00000000..d2545cbf --- /dev/null +++ b/front/packages/ui/src/browse/index.tsx @@ -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 . + */ + +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): WithLoading> => { + 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 => ({ + 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 ( + <> + {/* */} + + {(item, i) => } + + + ); +}; + +BrowsePage.getLayout = DefaultLayout; + +BrowsePage.getFetchUrls = ({ slug, sortBy }) => [ + query(slug, sortBy?.split("-")[0] as SortBy, sortBy?.split("-")[1] as SortOrd), +]; diff --git a/front/packages/ui/src/browse/list.tsx b/front/packages/ui/src/browse/list.tsx new file mode 100644 index 00000000..bc34647f --- /dev/null +++ b/front/packages/ui/src/browse/list.tsx @@ -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 . + */ + +const ItemList = ({ + href, + name, + subtitle, + thumbnail, + poster, + loading, +}: { + href?: string; + name?: string; + subtitle?: string | null; + poster?: string | null; + thumbnail?: string | null; + loading?: boolean; +}) => { + return ( + + {name} + + + {name ?? } + + {(loading || subtitle) && ( + + {subtitle ?? } + + )} + + + + ); +}; diff --git a/front/packages/ui/src/browse/toto.tsx b/front/packages/ui/src/browse/toto.tsx new file mode 100644 index 00000000..2a706490 --- /dev/null +++ b/front/packages/ui/src/browse/toto.tsx @@ -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 . + */ + +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 ( + + ); + case Layout.List: + return ( + + ); + } +}; + +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 ( + + {Object.values(SortBy).map((x) => ( + setSort(x)} + component={Link} + to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} + shallow + replace + > + {t(`browse.sortkey.${x}`)} + + ))} + + setSortOrd(SortOrd.Asc)} + component={Link} + to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} + shallow + replace + > + + + + {t("browse.sortord.asc")} + + setSortOrd(SortOrd.Desc)} + component={Link} + to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} + shallow + replace + > + + + + {t("browse.sortord.desc")} + + + ); +}; + +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(null); + const { t } = useTranslation("browse"); + + const switchViewTitle = + layout === Layout.Grid ? t("browse.switchToList") : t("browse.switchToGrid"); + + return ( + <> + + + + + + + + + + + + {sortAnchor && ( + setSortAnchor(null)} + /> + )} + + ); +}; diff --git a/front/packages/ui/src/browse/types.ts b/front/packages/ui/src/browse/types.ts new file mode 100644 index 00000000..0ceca512 --- /dev/null +++ b/front/packages/ui/src/browse/types.ts @@ -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 . + */ + +export enum SortBy { + Name = "name", + StartAir = "startAir", + EndAir = "endAir", +} + +export enum SortOrd { + Asc = "asc", + Desc = "desc", +} + +export enum Layout { + Grid, + List, +} diff --git a/front/packages/ui/src/fetch.tsx b/front/packages/ui/src/fetch.tsx index d4d7b437..925547ec 100644 --- a/front/packages/ui/src/fetch.tsx +++ b/front/packages/ui/src/fetch.tsx @@ -18,12 +18,14 @@ * along with Kyoo. If not, see . */ -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 & { isLoading: false }) | { isLoading: true }; +export type WithLoading = + | (Item & { isLoading: false }) + | (Partial & { isLoading: true }); const isPage = (obj: unknown): obj is Page => (typeof obj === "object" && obj && "items" in obj) || false; @@ -52,6 +54,29 @@ export const Fetch = ({ return <>{data.items.map((item, i) => children({ ...item, isLoading: false } as any, i))}; }; +export const InfiniteFetch = ({ + query, + placeholderCount = 15, + children, +}: { + query: QueryIdentifier; + placeholderCount?: number; + children: ( + item: Data extends Page ? WithLoading : WithLoading, + 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 ; + 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) => ( diff --git a/front/packages/ui/src/index.ts b/front/packages/ui/src/index.ts index 3ce76371..117a3655 100644 --- a/front/packages/ui/src/index.ts +++ b/front/packages/ui/src/index.ts @@ -19,3 +19,4 @@ */ export * from "./navbar"; +export { BrowsePage } from "./browse"; diff --git a/front/packages/ui/src/layout.tsx b/front/packages/ui/src/layout.tsx new file mode 100644 index 00000000..8f300e8e --- /dev/null +++ b/front/packages/ui/src/layout.tsx @@ -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 . + */ + +import { ReactElement } from "react"; +import { Navbar } from "./navbar"; +import { useYoshiki } from "yoshiki"; + +export const DefaultLayout = (page: ReactElement) => { + const { css } = useYoshiki(); + + return ( + <> + +
{page}
+ + ); +}; + +DefaultLayout.query = () => [Navbar.query()];