mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-20 05:25:36 +00:00
Compare commits
5 Commits
renovate/p
...
copilot/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdfef4c6bb | ||
|
|
c7c3cd720b | ||
|
|
e6859a1fff | ||
|
|
cc60dece54 | ||
|
|
7d2e050ed6 |
@@ -1,34 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { Show } from "~/models";
|
||||
import { P } from "~/primitives";
|
||||
import { Fetch, prefetch, type QueryIdentifier } from "~/query";
|
||||
import { HomePage, loader } from "~/ui/home";
|
||||
|
||||
export async function loader() {
|
||||
await prefetch(Header.query());
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<P>{t("home.recommended")}</P>
|
||||
<Fetch
|
||||
query={Header.query()}
|
||||
Render={({ name }) => <P {...(css({ bg: "red" }) as any)}>{name}</P>}
|
||||
Loader={() => <P>Loading</P>}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
Header.query = (): QueryIdentifier<Show> => ({
|
||||
parser: Show,
|
||||
path: ["shows", "random"],
|
||||
params: {
|
||||
fields: ["firstEntry"],
|
||||
},
|
||||
});
|
||||
export { loader };
|
||||
export default HomePage;
|
||||
|
||||
@@ -33,6 +33,27 @@ const Base = z.object({
|
||||
playedDate: zdate().nullable(),
|
||||
videoId: z.string().nullable(),
|
||||
}),
|
||||
// Optional fields for API responses
|
||||
serie: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
slug: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
watchStatus: z
|
||||
.object({
|
||||
status: z.enum([
|
||||
"completed",
|
||||
"watching",
|
||||
"rewatching",
|
||||
"dropped",
|
||||
"planned",
|
||||
]),
|
||||
percent: z.number().int().gte(0).lte(100),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const Episode = Base.extend({
|
||||
|
||||
@@ -18,21 +18,14 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Genre,
|
||||
type LibraryItem,
|
||||
LibraryItemP,
|
||||
type QueryIdentifier,
|
||||
useInfiniteFetch,
|
||||
} from "@kyoo/models";
|
||||
import { H3, ts } from "@kyoo/primitives";
|
||||
import { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { itemMap } from "../browse";
|
||||
import { ItemGrid } from "../browse/grid";
|
||||
import { InfiniteFetchList } from "../fetch-infinite";
|
||||
import { ItemGrid, itemMap } from "~/components/items";
|
||||
import type { Genre, Show } from "~/models";
|
||||
import { H3, ts } from "~/primitives";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
|
||||
export const Header = ({ title }: { title: string }) => {
|
||||
const { css } = useYoshiki();
|
||||
@@ -48,32 +41,19 @@ export const Header = ({ title }: { title: string }) => {
|
||||
})}
|
||||
>
|
||||
<H3>{title}</H3>
|
||||
{/* <View {...css({ flexDirection: "row" })}> */}
|
||||
{/* <IconButton */}
|
||||
{/* icon={ChevronLeft} */}
|
||||
{/* // onPress={() => ref.current?.scrollTo({ x: 0, animated: true })} */}
|
||||
{/* /> */}
|
||||
{/* <IconButton */}
|
||||
{/* icon={ChevronRight} */}
|
||||
{/* // onPress={() => ref.current?.scrollTo({ x: 0, animated: true })} */}
|
||||
{/* /> */}
|
||||
{/* </View> */}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const GenreGrid = ({ genre }: { genre: Genre }) => {
|
||||
const query = useInfiniteFetch(GenreGrid.query(genre));
|
||||
const displayEmpty = useRef(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{(displayEmpty.current || query.items?.length !== 0) && (
|
||||
<Header title={t(`genres.${genre}`)} />
|
||||
)}
|
||||
<InfiniteFetchList
|
||||
query={query}
|
||||
<Header title={t(`genres.${genre}`)} />
|
||||
<InfiniteFetch
|
||||
query={GenreGrid.query(genre)}
|
||||
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
|
||||
placeholderCount={2}
|
||||
empty={displayEmpty.current ? t("home.none") : undefined}
|
||||
@@ -84,15 +64,15 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
|
||||
);
|
||||
};
|
||||
|
||||
GenreGrid.query = (genre: Genre): QueryIdentifier<LibraryItem> => ({
|
||||
parser: LibraryItemP,
|
||||
GenreGrid.query = (genre: Genre): QueryIdentifier<Show> => ({
|
||||
parser: Show,
|
||||
infinite: true,
|
||||
path: ["items"],
|
||||
path: ["api", "shows"],
|
||||
params: {
|
||||
fields: ["watchStatus", "episodesCount"],
|
||||
fields: ["watchStatus"],
|
||||
filter: `genres has ${genre}`,
|
||||
sortBy: "random",
|
||||
// Limit the inital numbers of items
|
||||
sort: "random",
|
||||
// Limit the initial numbers of items
|
||||
limit: 10,
|
||||
},
|
||||
});
|
||||
@@ -18,45 +18,45 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type KyooImage, type LibraryItem, LibraryItemP, type QueryIdentifier } from "@kyoo/models";
|
||||
import Info from "@material-symbols/svg-400/rounded/info.svg";
|
||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import type { KImage, Show } from "~/models";
|
||||
import {
|
||||
GradientImageBackground,
|
||||
H1,
|
||||
H2,
|
||||
IconButton,
|
||||
IconFab,
|
||||
ImageBackground,
|
||||
Link,
|
||||
P,
|
||||
Skeleton,
|
||||
tooltip,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import Info from "@material-symbols/svg-400/rounded/info.svg";
|
||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import { Header as DetailsHeader } from "../../../../src/ui/details/header";
|
||||
import type { WithLoading } from "../fetch";
|
||||
} from "~/primitives";
|
||||
import { type QueryIdentifier } from "~/query";
|
||||
import { Header as DetailsHeader } from "../details/header";
|
||||
|
||||
export const Header = ({
|
||||
isLoading,
|
||||
name,
|
||||
thumbnail,
|
||||
overview,
|
||||
description,
|
||||
tagline,
|
||||
link,
|
||||
infoLink,
|
||||
...props
|
||||
}: WithLoading<{
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
name: string;
|
||||
thumbnail: KyooImage | null;
|
||||
overview: string | null;
|
||||
thumbnail: KImage | null;
|
||||
description: string | null;
|
||||
tagline: string | null;
|
||||
link: string | null;
|
||||
infoLink: string;
|
||||
}>) => {
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -105,7 +105,7 @@ export const Header = ({
|
||||
<Skeleton lines={4} {...css({ marginTop: ts(1) })}>
|
||||
{isLoading || (
|
||||
<P numberOfLines={4} {...css({ display: { xs: "none", md: "flex" } })}>
|
||||
{overview}
|
||||
{description}
|
||||
</P>
|
||||
)}
|
||||
</Skeleton>
|
||||
@@ -114,10 +114,10 @@ export const Header = ({
|
||||
);
|
||||
};
|
||||
|
||||
Header.query = (): QueryIdentifier<LibraryItem> => ({
|
||||
parser: LibraryItemP,
|
||||
path: ["items", "random"],
|
||||
Header.query = (): QueryIdentifier<Show> => ({
|
||||
parser: Show,
|
||||
path: ["api", "shows", "random"],
|
||||
params: {
|
||||
fields: ["firstEpisode"],
|
||||
fields: ["firstEntry"],
|
||||
},
|
||||
});
|
||||
@@ -18,12 +18,10 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Genre, type QueryPage, toQueryKey } from "@kyoo/models";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { RefreshControl, ScrollView } from "react-native";
|
||||
import { Fetch } from "../fetch";
|
||||
import { DefaultLayout } from "../layout";
|
||||
import { Genre } from "~/models";
|
||||
import { Fetch, prefetch } from "~/query";
|
||||
import { GenreGrid } from "./genre";
|
||||
import { Header } from "./header";
|
||||
import { NewsList } from "./news";
|
||||
@@ -31,9 +29,21 @@ import { Recommended } from "./recommended";
|
||||
import { VerticalRecommended } from "./vertical";
|
||||
import { WatchlistList } from "./watchlist";
|
||||
|
||||
export const HomePage: QueryPage<{}, Genre> = ({ randomItems }) => {
|
||||
const queryClient = useQueryClient();
|
||||
export async function loader() {
|
||||
const randomItems = [...Object.values(Genre)];
|
||||
await Promise.all([
|
||||
prefetch(Header.query()),
|
||||
prefetch(WatchlistList.query()),
|
||||
prefetch(NewsList.query()),
|
||||
...randomItems.filter((_, i) => i < 6).map((x) => prefetch(GenreGrid.query(x))),
|
||||
prefetch(Recommended.query()),
|
||||
prefetch(VerticalRecommended.query()),
|
||||
]);
|
||||
}
|
||||
|
||||
export const HomePage = () => {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const randomItems = [...Object.values(Genre)];
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -41,34 +51,38 @@ export const HomePage: QueryPage<{}, Genre> = ({ randomItems }) => {
|
||||
<RefreshControl
|
||||
onRefresh={async () => {
|
||||
setRefreshing(true);
|
||||
await Promise.all(
|
||||
HomePage.getFetchUrls!({}, randomItems).map((query) =>
|
||||
queryClient.refetchQueries({
|
||||
queryKey: toQueryKey(query),
|
||||
type: "active",
|
||||
exact: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
await loader();
|
||||
setRefreshing(false);
|
||||
}}
|
||||
refreshing={refreshing}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Fetch query={Header.query()}>
|
||||
{(x) => (
|
||||
<Fetch
|
||||
query={Header.query()}
|
||||
Render={(x) => (
|
||||
<Header
|
||||
isLoading={x.isLoading as any}
|
||||
isLoading={false}
|
||||
name={x.name}
|
||||
tagline={"tagline" in x ? x.tagline : null}
|
||||
overview={x.overview}
|
||||
tagline={x.kind !== "collection" && "tagline" in x ? x.tagline : null}
|
||||
description={x.description}
|
||||
thumbnail={x.thumbnail}
|
||||
link={x.kind !== "collection" && !x.isLoading ? x.playHref : undefined}
|
||||
link={x.kind !== "collection" ? x.playHref : null}
|
||||
infoLink={x.href}
|
||||
/>
|
||||
)}
|
||||
</Fetch>
|
||||
Loader={() => (
|
||||
<Header
|
||||
isLoading={true}
|
||||
name=""
|
||||
tagline={null}
|
||||
description={null}
|
||||
thumbnail={null}
|
||||
link={null}
|
||||
infoLink="#"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<WatchlistList />
|
||||
<NewsList />
|
||||
{randomItems
|
||||
@@ -90,16 +104,3 @@ export const HomePage: QueryPage<{}, Genre> = ({ randomItems }) => {
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
HomePage.randomItems = [...Object.values(Genre)];
|
||||
|
||||
HomePage.getLayout = { Layout: DefaultLayout, props: { transparent: true } };
|
||||
|
||||
HomePage.getFetchUrls = (_, randomItems) => [
|
||||
Header.query(),
|
||||
WatchlistList.query(),
|
||||
NewsList.query(),
|
||||
...randomItems.filter((_, i) => i < 6).map((x) => GenreGrid.query(x)),
|
||||
Recommended.query(),
|
||||
VerticalRecommended.query(),
|
||||
];
|
||||
@@ -18,12 +18,12 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type News, NewsP, type QueryIdentifier, getDisplayDate } from "@kyoo/models";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { ItemGrid } from "../browse/grid";
|
||||
import { EpisodeBox, episodeDisplayNumber } from "../../../../src/ui/details/episode";
|
||||
import { InfiniteFetch } from "../fetch-infinite";
|
||||
import { EntryBox, entryDisplayNumber } from "~/components/entries";
|
||||
import { ItemGrid } from "~/components/items";
|
||||
import type { Entry } from "~/models";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
import { Header } from "./genre";
|
||||
|
||||
export const NewsList = () => {
|
||||
@@ -40,17 +40,16 @@ export const NewsList = () => {
|
||||
getItemSize={(kind) => (kind === "episode" ? 2 : 1)}
|
||||
empty={t("home.none")}
|
||||
Render={({ item }) => {
|
||||
if (item.kind === "episode") {
|
||||
if (item.kind === "episode" || item.kind === "special") {
|
||||
return (
|
||||
<EpisodeBox
|
||||
<EntryBox
|
||||
slug={item.slug}
|
||||
showSlug={item.show!.slug}
|
||||
name={`${item.show!.name} ${episodeDisplayNumber(item)}`}
|
||||
overview={item.name}
|
||||
serieSlug={item.serie!.slug}
|
||||
name={`${item.serie!.name} ${entryDisplayNumber(item)}`}
|
||||
description={item.name}
|
||||
thumbnail={item.thumbnail}
|
||||
href={item.href}
|
||||
watchedPercent={item.watchStatus?.watchedPercent || null}
|
||||
watchedStatus={item.watchStatus?.status || null}
|
||||
href={item.href ?? "#"}
|
||||
watchedPercent={item.watchStatus?.percent || null}
|
||||
// TODO: Move this into the ItemList (using getItemSize)
|
||||
// @ts-expect-error This is a web only property
|
||||
{...css({ gridColumnEnd: "span 2" })}
|
||||
@@ -59,31 +58,31 @@ export const NewsList = () => {
|
||||
}
|
||||
return (
|
||||
<ItemGrid
|
||||
href={item.href}
|
||||
href={item.href ?? "#"}
|
||||
slug={item.slug}
|
||||
kind={"movie"}
|
||||
name={item.name!}
|
||||
subtitle={getDisplayDate(item)}
|
||||
poster={item.poster}
|
||||
subtitle={item.airDate ? new Date(item.airDate).getFullYear().toString() : null}
|
||||
poster={item.kind === "movie" ? item.poster : null}
|
||||
watchStatus={item.watchStatus?.status || null}
|
||||
watchPercent={item.watchStatus?.watchedPercent || null}
|
||||
watchPercent={item.watchStatus?.percent || null}
|
||||
unseenEpisodesCount={null}
|
||||
type={"movie"}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
Loader={({ index }) => (index % 2 ? <EpisodeBox.Loader /> : <ItemGrid.Loader />)}
|
||||
Loader={({ index }) => (index % 2 ? <EntryBox.Loader /> : <ItemGrid.Loader />)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
NewsList.query = (): QueryIdentifier<News> => ({
|
||||
parser: NewsP,
|
||||
NewsList.query = (): QueryIdentifier<Entry> => ({
|
||||
parser: Entry,
|
||||
infinite: true,
|
||||
path: ["news"],
|
||||
path: ["api", "news"],
|
||||
params: {
|
||||
// Limit the initial numbers of items
|
||||
limit: 10,
|
||||
fields: ["show", "watchStatus"],
|
||||
fields: ["serie", "watchStatus"],
|
||||
},
|
||||
});
|
||||
@@ -18,47 +18,42 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
type Genre,
|
||||
type KyooImage,
|
||||
type LibraryItem,
|
||||
LibraryItemP,
|
||||
type QueryIdentifier,
|
||||
type WatchStatusV,
|
||||
getDisplayDate,
|
||||
} from "@kyoo/models";
|
||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { type Theme, calc, percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
import { ItemGrid, ItemWatchStatus } from "~/components/items";
|
||||
import { ItemContext } from "~/components/items/context-menus";
|
||||
import type { Genre, KImage, Show, WatchStatusV } from "~/models";
|
||||
import { getDisplayDate } from "~/utils";
|
||||
import {
|
||||
Chip,
|
||||
H3,
|
||||
IconFab,
|
||||
Link,
|
||||
P,
|
||||
Poster,
|
||||
PosterBackground,
|
||||
Skeleton,
|
||||
SubP,
|
||||
focusReset,
|
||||
imageBorderRadius,
|
||||
tooltip,
|
||||
ts,
|
||||
} from "@kyoo/primitives";
|
||||
import PlayArrow from "@material-symbols/svg-400/rounded/play_arrow-fill.svg";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { type Theme, calc, percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
import { ItemGrid, ItemWatchStatus } from "../browse/grid";
|
||||
import { ItemContext } from "../../../../src/ui/info/components/context-menus";
|
||||
import type { Layout } from "../fetch";
|
||||
import { InfiniteFetch } from "../fetch-infinite";
|
||||
} from "~/primitives";
|
||||
import { InfiniteFetch, type Layout, type QueryIdentifier } from "~/query";
|
||||
|
||||
const imageBorderRadius = 6;
|
||||
const focusReset = {
|
||||
boxShadow: "unset",
|
||||
outline: "none",
|
||||
};
|
||||
|
||||
export const ItemDetails = ({
|
||||
slug,
|
||||
type,
|
||||
kind,
|
||||
name,
|
||||
tagline,
|
||||
subtitle,
|
||||
overview,
|
||||
description,
|
||||
poster,
|
||||
genres,
|
||||
href,
|
||||
@@ -68,13 +63,13 @@ export const ItemDetails = ({
|
||||
...props
|
||||
}: {
|
||||
slug: string;
|
||||
type: "movie" | "show" | "collection";
|
||||
kind: "movie" | "serie" | "collection";
|
||||
name: string;
|
||||
tagline: string | null;
|
||||
subtitle: string | null;
|
||||
poster: KyooImage | null;
|
||||
poster: KImage | null;
|
||||
genres: Genre[] | null;
|
||||
overview: string | null;
|
||||
description: string | null;
|
||||
href: string;
|
||||
playHref: string | null;
|
||||
watchStatus: WatchStatusV | null;
|
||||
@@ -152,9 +147,9 @@ export const ItemDetails = ({
|
||||
alignContent: "flex-start",
|
||||
})}
|
||||
>
|
||||
{type !== "collection" && (
|
||||
{kind !== "collection" && (
|
||||
<ItemContext
|
||||
type={type}
|
||||
kind={kind}
|
||||
slug={slug}
|
||||
status={watchStatus}
|
||||
isOpen={moreOpened}
|
||||
@@ -165,7 +160,7 @@ export const ItemDetails = ({
|
||||
{tagline && <P {...css({ p: ts(1) })}>{tagline}</P>}
|
||||
</View>
|
||||
<ScrollView {...css({ pX: ts(1) })}>
|
||||
<SubP {...css({ textAlign: "justify" })}>{overview ?? t("show.noOverview")}</SubP>
|
||||
<SubP {...css({ textAlign: "justify" })}>{description ?? t("show.noOverview")}</SubP>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Link>
|
||||
@@ -231,9 +226,12 @@ ItemDetails.Loader = (props: object) => {
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<Poster.Loader
|
||||
<PosterBackground
|
||||
src={null}
|
||||
alt=""
|
||||
quality="low"
|
||||
layout={{ height: percent(100) }}
|
||||
{...css({ borderTopRightRadius: 0, borderBottomRightRadius: 0 })}
|
||||
style={{ borderTopRightRadius: 0, borderBottomRightRadius: 0 }}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
@@ -248,7 +246,7 @@ ItemDetails.Loader = (props: object) => {
|
||||
<Skeleton {...css({ width: percent(100) })} />
|
||||
<Skeleton {...css({ height: rem(0.8) })} />
|
||||
</View>
|
||||
</Poster.Loader>
|
||||
</PosterBackground>
|
||||
<View {...css({ flexShrink: 1, flexGrow: 1 })}>
|
||||
<View {...css({ flexGrow: 1, flexShrink: 1, pX: ts(1) })}>
|
||||
<Skeleton {...css({ marginVertical: ts(2) })} />
|
||||
@@ -295,19 +293,19 @@ export const Recommended = () => {
|
||||
Render={({ item }) => (
|
||||
<ItemDetails
|
||||
slug={item.slug}
|
||||
type={item.kind}
|
||||
kind={item.kind}
|
||||
name={item.name}
|
||||
tagline={"tagline" in item ? item.tagline : null}
|
||||
overview={item.overview}
|
||||
tagline={item.kind !== "collection" && "tagline" in item ? item.tagline : null}
|
||||
description={item.description}
|
||||
poster={item.poster}
|
||||
subtitle={item.kind !== "collection" ? getDisplayDate(item) : null}
|
||||
genres={"genres" in item ? item.genres : null}
|
||||
genres={item.kind !== "collection" && "genres" in item ? item.genres : null}
|
||||
href={item.href}
|
||||
playHref={item.kind !== "collection" ? item.playHref : null}
|
||||
watchStatus={(item.kind !== "collection" && item.watchStatus?.status) || null}
|
||||
unseenEpisodesCount={
|
||||
item.kind === "show"
|
||||
? (item.watchStatus?.unseenEpisodesCount ?? item.episodesCount!)
|
||||
item.kind === "serie"
|
||||
? (item.availableCount - (item.watchStatus?.seenCount ?? 0))
|
||||
: null
|
||||
}
|
||||
/>
|
||||
@@ -318,13 +316,13 @@ export const Recommended = () => {
|
||||
);
|
||||
};
|
||||
|
||||
Recommended.query = (): QueryIdentifier<LibraryItem> => ({
|
||||
parser: LibraryItemP,
|
||||
Recommended.query = (): QueryIdentifier<Show> => ({
|
||||
parser: Show,
|
||||
infinite: true,
|
||||
path: ["items"],
|
||||
path: ["api", "shows"],
|
||||
params: {
|
||||
sortBy: "random",
|
||||
sort: "random",
|
||||
limit: 6,
|
||||
fields: ["firstEpisode", "episodesCount", "watchStatus"],
|
||||
fields: ["firstEntry", "watchStatus"],
|
||||
},
|
||||
});
|
||||
@@ -18,15 +18,13 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type LibraryItem, LibraryItemP, type QueryIdentifier } from "@kyoo/models";
|
||||
import { H3 } from "@kyoo/primitives";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { itemMap } from "../browse";
|
||||
import { ItemGrid } from "../browse/grid";
|
||||
import { ItemList } from "../browse/list";
|
||||
import { InfiniteFetch } from "../fetch-infinite";
|
||||
import { ItemGrid, ItemList, itemMap } from "~/components/items";
|
||||
import type { Show } from "~/models";
|
||||
import { H3 } from "~/primitives";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
|
||||
export const VerticalRecommended = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -48,13 +46,13 @@ export const VerticalRecommended = () => {
|
||||
);
|
||||
};
|
||||
|
||||
VerticalRecommended.query = (): QueryIdentifier<LibraryItem> => ({
|
||||
parser: LibraryItemP,
|
||||
VerticalRecommended.query = (): QueryIdentifier<Show> => ({
|
||||
parser: Show,
|
||||
infinite: true,
|
||||
path: ["items"],
|
||||
path: ["api", "shows"],
|
||||
params: {
|
||||
fields: ["episodesCount", "watchStatus"],
|
||||
sortBy: "random",
|
||||
fields: ["watchStatus"],
|
||||
sort: "random",
|
||||
limit: 3,
|
||||
},
|
||||
});
|
||||
@@ -18,20 +18,16 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
type QueryIdentifier,
|
||||
type Watchlist,
|
||||
WatchlistP,
|
||||
getDisplayDate,
|
||||
useAccount,
|
||||
} from "@kyoo/models";
|
||||
import { Button, P, ts } from "@kyoo/primitives";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { ItemGrid } from "../browse/grid";
|
||||
import { EpisodeBox, episodeDisplayNumber } from "../../../../src/ui/details/episode";
|
||||
import { InfiniteFetch } from "../fetch-infinite";
|
||||
import { EntryBox, entryDisplayNumber } from "~/components/entries";
|
||||
import { ItemGrid } from "~/components/items";
|
||||
import type { Show } from "~/models";
|
||||
import { getDisplayDate } from "~/utils";
|
||||
import { Button, P, ts } from "~/primitives";
|
||||
import { useAccount } from "~/providers/account-context";
|
||||
import { InfiniteFetch, type QueryIdentifier } from "~/query";
|
||||
import { Header } from "./genre";
|
||||
|
||||
export const WatchlistList = () => {
|
||||
@@ -62,23 +58,22 @@ export const WatchlistList = () => {
|
||||
query={WatchlistList.query()}
|
||||
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
|
||||
getItemType={(x, i) =>
|
||||
(x?.kind === "show" && x.watchStatus?.nextEpisode) || (!x && i % 2) ? "episode" : "item"
|
||||
(x?.kind === "serie" && x.nextEntry) || (!x && i % 2) ? "episode" : "item"
|
||||
}
|
||||
getItemSize={(kind) => (kind === "episode" ? 2 : 1)}
|
||||
empty={t("home.none")}
|
||||
Render={({ item }) => {
|
||||
const episode = item.kind === "show" ? item.watchStatus?.nextEpisode : null;
|
||||
if (episode) {
|
||||
const entry = item.kind === "serie" ? item.nextEntry : null;
|
||||
if (entry) {
|
||||
return (
|
||||
<EpisodeBox
|
||||
slug={episode.slug}
|
||||
showSlug={item.slug}
|
||||
name={`${item.name} ${episodeDisplayNumber(episode)}`}
|
||||
overview={episode.name}
|
||||
thumbnail={episode.thumbnail ?? item.thumbnail}
|
||||
href={episode.href}
|
||||
watchedPercent={item.watchStatus?.watchedPercent || null}
|
||||
watchedStatus={item.watchStatus?.status || null}
|
||||
<EntryBox
|
||||
slug={entry.slug}
|
||||
serieSlug={item.slug}
|
||||
name={`${item.name} ${entryDisplayNumber(entry)}`}
|
||||
description={entry.name}
|
||||
thumbnail={entry.thumbnail ?? item.thumbnail}
|
||||
href={entry.href ?? "#"}
|
||||
watchedPercent={entry.watchStatus?.percent || null}
|
||||
// TODO: Move this into the ItemList (using getItemSize)
|
||||
// @ts-expect-error This is a web only property
|
||||
{...css({ gridColumnEnd: "span 2" })}
|
||||
@@ -89,31 +84,29 @@ export const WatchlistList = () => {
|
||||
<ItemGrid
|
||||
href={item.href}
|
||||
slug={item.slug}
|
||||
kind={item.kind}
|
||||
name={item.name!}
|
||||
subtitle={getDisplayDate(item)}
|
||||
poster={item.poster}
|
||||
watchStatus={item.watchStatus?.status || null}
|
||||
watchPercent={item.watchStatus?.watchedPercent || null}
|
||||
unseenEpisodesCount={
|
||||
(item.kind === "show" && item.watchStatus?.unseenEpisodesCount) || null
|
||||
}
|
||||
type={item.kind}
|
||||
watchPercent={item.kind === "movie" && item.watchStatus ? item.watchStatus.percent : null}
|
||||
unseenEpisodesCount={null}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
Loader={({ index }) => (index % 2 ? <EpisodeBox.Loader /> : <ItemGrid.Loader />)}
|
||||
Loader={({ index }) => (index % 2 ? <EntryBox.Loader /> : <ItemGrid.Loader />)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
WatchlistList.query = (): QueryIdentifier<Watchlist> => ({
|
||||
parser: WatchlistP,
|
||||
WatchlistList.query = (): QueryIdentifier<Show> => ({
|
||||
parser: Show,
|
||||
infinite: true,
|
||||
path: ["watchlist"],
|
||||
path: ["api", "watchlist"],
|
||||
params: {
|
||||
// Limit the inital numbers of items
|
||||
// Limit the initial numbers of items
|
||||
limit: 10,
|
||||
fields: ["watchStatus"],
|
||||
fields: ["watchStatus", "nextEntry"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user