From f8d935becd1dbf1665d1d86f847f4f340254a626 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 20 Dec 2025 22:09:27 +0100 Subject: [PATCH] Update home page for v5 --- front/src/app/(app)/(tabs)/index.tsx | 36 +------- front/src/models/entry.ts | 21 +++++ .../ui/src => src/ui}/home/genre.tsx | 46 +++------- .../ui/src => src/ui}/home/header.tsx | 40 ++++----- .../ui/src => src/ui}/home/index.tsx | 71 +++++++-------- .../{packages/ui/src => src/ui}/home/news.tsx | 43 +++++----- .../ui/src => src/ui}/home/recommended.tsx | 86 +++++++++---------- .../ui/src => src/ui}/home/vertical.tsx | 20 ++--- .../ui/src => src/ui}/home/watchlist.tsx | 61 ++++++------- 9 files changed, 192 insertions(+), 232 deletions(-) rename front/{packages/ui/src => src/ui}/home/genre.tsx (61%) rename front/{packages/ui/src => src/ui}/home/header.tsx (84%) rename front/{packages/ui/src => src/ui}/home/index.tsx (62%) rename front/{packages/ui/src => src/ui}/home/news.tsx (66%) rename front/{packages/ui/src => src/ui}/home/recommended.tsx (84%) rename front/{packages/ui/src => src/ui}/home/vertical.tsx (76%) rename front/{packages/ui/src => src/ui}/home/watchlist.tsx (62%) 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"], }, });