diff --git a/front/src/app/(app)/(tabs)/index.tsx b/front/src/app/(app)/(tabs)/index.tsx
index 59856281..82572cea 100644
--- a/front/src/app/(app)/(tabs)/index.tsx
+++ b/front/src/app/(app)/(tabs)/index.tsx
@@ -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 (
-
- {t("home.recommended")}
- {name}
}
- Loader={() => Loading
}
- />
-
- );
-}
-
-Header.query = (): QueryIdentifier => ({
- parser: Show,
- path: ["shows", "random"],
- params: {
- fields: ["firstEntry"],
- },
-});
+export { loader };
+export default HomePage;
diff --git a/front/src/models/entry.ts b/front/src/models/entry.ts
index 3680efe2..fd3c8ba5 100644
--- a/front/src/models/entry.ts
+++ b/front/src/models/entry.ts
@@ -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({
diff --git a/front/packages/ui/src/home/genre.tsx b/front/src/ui/home/genre.tsx
similarity index 61%
rename from front/packages/ui/src/home/genre.tsx
rename to front/src/ui/home/genre.tsx
index ae9c711d..be3e0656 100644
--- a/front/packages/ui/src/home/genre.tsx
+++ b/front/src/ui/home/genre.tsx
@@ -18,21 +18,14 @@
* along with Kyoo. If not, see .
*/
-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 }) => {
})}
>
{title}
- {/* */}
- {/* ref.current?.scrollTo({ x: 0, animated: true })} */}
- {/* /> */}
- {/* ref.current?.scrollTo({ x: 0, animated: true })} */}
- {/* /> */}
- {/* */}
);
};
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) && (
-
- )}
-
+ {
);
};
-GenreGrid.query = (genre: Genre): QueryIdentifier => ({
- parser: LibraryItemP,
+GenreGrid.query = (genre: Genre): QueryIdentifier => ({
+ 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,
},
});
diff --git a/front/packages/ui/src/home/header.tsx b/front/src/ui/home/header.tsx
similarity index 84%
rename from front/packages/ui/src/home/header.tsx
rename to front/src/ui/home/header.tsx
index 42790e40..2cea238b 100644
--- a/front/packages/ui/src/home/header.tsx
+++ b/front/src/ui/home/header.tsx
@@ -18,45 +18,45 @@
* along with Kyoo. If not, see .
*/
-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 = ({
{isLoading || (
- {overview}
+ {description}
)}
@@ -114,10 +114,10 @@ export const Header = ({
);
};
-Header.query = (): QueryIdentifier => ({
- parser: LibraryItemP,
- path: ["items", "random"],
+Header.query = (): QueryIdentifier => ({
+ parser: Show,
+ path: ["api", "shows", "random"],
params: {
- fields: ["firstEpisode"],
+ fields: ["firstEntry"],
},
});
diff --git a/front/packages/ui/src/home/index.tsx b/front/src/ui/home/index.tsx
similarity index 62%
rename from front/packages/ui/src/home/index.tsx
rename to front/src/ui/home/index.tsx
index 5ffff2db..ad18b675 100644
--- a/front/packages/ui/src/home/index.tsx
+++ b/front/src/ui/home/index.tsx
@@ -18,12 +18,10 @@
* along with Kyoo. If not, see .
*/
-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 (
= ({ randomItems }) => {
{
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}
/>
}
>
-
- {(x) => (
+ (
)}
-
+ Loader={() => (
+
+ )}
+ />
{randomItems
@@ -90,16 +104,3 @@ export const HomePage: QueryPage<{}, Genre> = ({ randomItems }) => {
);
};
-
-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(),
-];
diff --git a/front/packages/ui/src/home/news.tsx b/front/src/ui/home/news.tsx
similarity index 66%
rename from front/packages/ui/src/home/news.tsx
rename to front/src/ui/home/news.tsx
index ecaf4804..70109118 100644
--- a/front/packages/ui/src/home/news.tsx
+++ b/front/src/ui/home/news.tsx
@@ -18,12 +18,12 @@
* along with Kyoo. If not, see .
*/
-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 (
- {
}
return (
);
}}
- Loader={({ index }) => (index % 2 ? : )}
+ Loader={({ index }) => (index % 2 ? : )}
/>
>
);
};
-NewsList.query = (): QueryIdentifier => ({
- parser: NewsP,
+NewsList.query = (): QueryIdentifier => ({
+ parser: Entry,
infinite: true,
- path: ["news"],
+ path: ["api", "news"],
params: {
// Limit the initial numbers of items
limit: 10,
- fields: ["show", "watchStatus"],
+ fields: ["serie", "watchStatus"],
},
});
diff --git a/front/packages/ui/src/home/recommended.tsx b/front/src/ui/home/recommended.tsx
similarity index 84%
rename from front/packages/ui/src/home/recommended.tsx
rename to front/src/ui/home/recommended.tsx
index 9d657091..7615c926 100644
--- a/front/packages/ui/src/home/recommended.tsx
+++ b/front/src/ui/home/recommended.tsx
@@ -18,47 +18,42 @@
* along with Kyoo. If not, see .
*/
-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" && (
{tagline}
}
- {overview ?? t("show.noOverview")}
+ {description ?? t("show.noOverview")}
@@ -231,9 +226,12 @@ ItemDetails.Loader = (props: object) => {
props,
)}
>
-
{
-
+
@@ -295,19 +293,19 @@ export const Recommended = () => {
Render={({ item }) => (
@@ -318,13 +316,13 @@ export const Recommended = () => {
);
};
-Recommended.query = (): QueryIdentifier => ({
- parser: LibraryItemP,
+Recommended.query = (): QueryIdentifier => ({
+ parser: Show,
infinite: true,
- path: ["items"],
+ path: ["api", "shows"],
params: {
- sortBy: "random",
+ sort: "random",
limit: 6,
- fields: ["firstEpisode", "episodesCount", "watchStatus"],
+ fields: ["firstEntry", "watchStatus"],
},
});
diff --git a/front/packages/ui/src/home/vertical.tsx b/front/src/ui/home/vertical.tsx
similarity index 76%
rename from front/packages/ui/src/home/vertical.tsx
rename to front/src/ui/home/vertical.tsx
index 246ebe26..5b27abf5 100644
--- a/front/packages/ui/src/home/vertical.tsx
+++ b/front/src/ui/home/vertical.tsx
@@ -18,15 +18,13 @@
* along with Kyoo. If not, see .
*/
-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 => ({
- parser: LibraryItemP,
+VerticalRecommended.query = (): QueryIdentifier => ({
+ parser: Show,
infinite: true,
- path: ["items"],
+ path: ["api", "shows"],
params: {
- fields: ["episodesCount", "watchStatus"],
- sortBy: "random",
+ fields: ["watchStatus"],
+ sort: "random",
limit: 3,
},
});
diff --git a/front/packages/ui/src/home/watchlist.tsx b/front/src/ui/home/watchlist.tsx
similarity index 62%
rename from front/packages/ui/src/home/watchlist.tsx
rename to front/src/ui/home/watchlist.tsx
index 58532dc4..73709507 100644
--- a/front/packages/ui/src/home/watchlist.tsx
+++ b/front/src/ui/home/watchlist.tsx
@@ -18,20 +18,16 @@
* along with Kyoo. If not, see .
*/
-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 (
- {
);
}}
- Loader={({ index }) => (index % 2 ? : )}
+ Loader={({ index }) => (index % 2 ? : )}
/>
>
);
};
-WatchlistList.query = (): QueryIdentifier => ({
- parser: WatchlistP,
+WatchlistList.query = (): QueryIdentifier => ({
+ 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"],
},
});