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