From 9f8b2da76e73a5673abd95ffe9beabf8112808ae Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 20 May 2024 17:52:55 +0200 Subject: [PATCH 1/9] Rework loader in infinite lists --- front/packages/ui/src/fetch-infinite.tsx | 15 ++++++--------- front/packages/ui/src/fetch-infinite.web.tsx | 13 ++++++------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/front/packages/ui/src/fetch-infinite.tsx b/front/packages/ui/src/fetch-infinite.tsx index 413f4558..1ff4231e 100644 --- a/front/packages/ui/src/fetch-infinite.tsx +++ b/front/packages/ui/src/fetch-infinite.tsx @@ -65,7 +65,8 @@ export const InfiniteFetchList = ( query, placeholderCount = 2, incremental = false, - children, + Render, + Loader, layout, empty, divider = false, @@ -82,10 +83,8 @@ export const InfiniteFetchList = ( placeholderCount?: number; layout: Layout; horizontal?: boolean; - children: ( - item: Data extends Page ? WithLoading : WithLoading, - i: number, - ) => ReactElement | null; + Render: (props: { item: Data; index: number }) => ReactElement | null; + Loader: (props: { index: number }) => ReactElement | null; empty?: string | JSX.Element; incremental?: boolean; divider?: boolean | ComponentType; @@ -111,9 +110,7 @@ export const InfiniteFetchList = ( if (incremental) items ??= oldItems.current; const count = items ? numColumns - (items.length % numColumns) : placeholderCount; - const placeholders = [...Array(count === 0 ? numColumns : count)].map( - (_, i) => ({ id: `gen${i}`, isLoading: true }) as Data, - ); + const placeholders = [...Array(count === 0 ? numColumns : count)].fill(null); const data = isFetching || !items ? [...(items || []), ...placeholders] : items; const List = nested ? (FlatList as unknown as typeof FlashList) : FlashList; @@ -137,7 +134,7 @@ export const InfiniteFetchList = ( }, ]} > - {children({ isLoading: false, ...item } as any, index)} + {item ? : } )} data={data} diff --git a/front/packages/ui/src/fetch-infinite.web.tsx b/front/packages/ui/src/fetch-infinite.web.tsx index 900d3a5f..61c33051 100644 --- a/front/packages/ui/src/fetch-infinite.web.tsx +++ b/front/packages/ui/src/fetch-infinite.web.tsx @@ -145,7 +145,7 @@ export const InfiniteFetchList = >; incremental?: boolean; placeholderCount?: number; layout: Layout; - children: ( - item: Data extends Page ? WithLoading : WithLoading, - i: number, - ) => ReactElement | null; + Render: (props: { item: Data; index: number }) => ReactElement | null; + Loader: (props: { index: number }) => ReactElement | null; empty?: string | JSX.Element; divider?: boolean | ComponentType; Header?: ComponentType<{ children: JSX.Element } & HeaderProps> | ReactElement; @@ -193,7 +192,7 @@ export const InfiniteFetchList = ( {Divider && i !== 0 && (Divider === true ?
: )} - {children({ isLoading: true } as any, i)} +
))} Header={Header} @@ -203,7 +202,7 @@ export const InfiniteFetchList = ( {Divider && i !== 0 && (Divider === true ?
: )} - {children({ ...item, isLoading: false } as any, i)} +
))} From 444de0af263cbc94bee9e39ff0e3d66add80a917 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 20 May 2024 17:54:13 +0200 Subject: [PATCH 2/9] Split loaders in the browse page --- front/packages/models/src/utils.ts | 1 + front/packages/primitives/src/image/image.tsx | 7 ++ .../primitives/src/image/image.web.tsx | 7 ++ front/packages/primitives/src/image/index.tsx | 7 ++ front/packages/ui/src/browse/grid.tsx | 79 +++++++----- front/packages/ui/src/browse/index.tsx | 40 +++--- front/packages/ui/src/browse/list.tsx | 115 +++++++++++------- 7 files changed, 154 insertions(+), 102 deletions(-) diff --git a/front/packages/models/src/utils.ts b/front/packages/models/src/utils.ts index 6d3557de..51678098 100644 --- a/front/packages/models/src/utils.ts +++ b/front/packages/models/src/utils.ts @@ -42,6 +42,7 @@ export const getDisplayDate = (data: Show | Movie) => { if (airDate) { return airDate.getFullYear().toString(); } + return null; }; export const useLocalSetting = (setting: string, def: string) => { diff --git a/front/packages/primitives/src/image/image.tsx b/front/packages/primitives/src/image/image.tsx index a117d76d..288a892f 100644 --- a/front/packages/primitives/src/image/image.tsx +++ b/front/packages/primitives/src/image/image.tsx @@ -93,3 +93,10 @@ export const Image = ({ ); }; + +Image.Loader = ({ layout, ...props }: { layout: ImageLayout }) => { + const { css } = useYoshiki(); + const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle; + + return ; +}; diff --git a/front/packages/primitives/src/image/image.web.tsx b/front/packages/primitives/src/image/image.web.tsx index 3c5f6514..7a459e55 100644 --- a/front/packages/primitives/src/image/image.web.tsx +++ b/front/packages/primitives/src/image/image.web.tsx @@ -73,3 +73,10 @@ export const Image = ({ ); }; + +Image.Loader = ({ layout, ...props }: { layout: ImageLayout }) => { + const { css } = useYoshiki(); + const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle; + + return ; +}; diff --git a/front/packages/primitives/src/image/index.tsx b/front/packages/primitives/src/image/index.tsx index daffe995..ca793d31 100644 --- a/front/packages/primitives/src/image/index.tsx +++ b/front/packages/primitives/src/image/index.tsx @@ -39,6 +39,13 @@ export const Poster = ({ layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; }) => {alt!}; +Poster.Loader = ({ + layout, + ...props +}: { + layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; +}) => ; + export const PosterBackground = ({ alt, layout, diff --git a/front/packages/ui/src/browse/grid.tsx b/front/packages/ui/src/browse/grid.tsx index cf708683..10c23a9d 100644 --- a/front/packages/ui/src/browse/grid.tsx +++ b/front/packages/ui/src/browse/grid.tsx @@ -23,6 +23,7 @@ import { Icon, Link, P, + Poster, PosterBackground, Skeleton, SubP, @@ -35,7 +36,7 @@ import { useState } from "react"; import { type ImageStyle, Platform, View } from "react-native"; import { type Stylable, type Theme, max, percent, px, rem, useYoshiki } from "yoshiki/native"; import { ItemContext } from "../components/context-menus"; -import type { Layout, WithLoading } from "../fetch"; +import type { Layout } from "../fetch"; export const ItemWatchStatus = ({ watchStatus, @@ -113,23 +114,21 @@ export const ItemGrid = ({ type, subtitle, poster, - isLoading, watchStatus, watchPercent, unseenEpisodesCount, ...props -}: WithLoading<{ +}: { href: string; slug: string; name: string; - subtitle?: string; - poster?: KyooImage | null; + subtitle: string | null; + poster: KyooImage | null; watchStatus: WatchStatusV | null; watchPercent: number | null; type: "movie" | "show" | "collection"; unseenEpisodesCount: number | null; -}> & - Stylable<"text">) => { +} & Stylable<"text">) => { const [moreOpened, setMoreOpened] = useState(false); const { css } = useYoshiki("grid"); @@ -172,13 +171,12 @@ export const ItemGrid = ({ src={poster} alt={name} quality="low" - forcedLoading={isLoading} layout={{ width: percent(100) }} {...(css("poster") as { style: ImageStyle })} > {type === "movie" && watchPercent && } - {slug && watchStatus !== undefined && type && type !== "collection" && ( + {type !== "collection" && ( )} - - {isLoading || ( -

- {name} -

- )} -
- {(isLoading || subtitle) && ( - - {isLoading || ( - - {subtitle} - - )} - +

+ {name} +

+ {subtitle && ( + + {subtitle} + )} ); }; +ItemGrid.Loader = (props: Stylable) => { + const { css } = useYoshiki(); + + return ( + + theme.background, + borderWidth: ts(0.5), + borderStyle: "solid", + })} + /> + + + + ); +}; + ItemGrid.layout = { size: px(150), numColumns: { xs: 3, sm: 4, md: 5, lg: 6, xl: 8 }, diff --git a/front/packages/ui/src/browse/index.tsx b/front/packages/ui/src/browse/index.tsx index 9b038f69..9863657e 100644 --- a/front/packages/ui/src/browse/index.tsx +++ b/front/packages/ui/src/browse/index.tsx @@ -27,7 +27,6 @@ import { } from "@kyoo/models"; import { type ComponentProps, useState } from "react"; import { createParam } from "solito"; -import type { WithLoading } from "../fetch"; import { InfiniteFetch } from "../fetch-infinite"; import { DefaultLayout } from "../layout"; import { ItemGrid } from "./grid"; @@ -38,25 +37,20 @@ import { Layout, SortBy, SortOrd } from "./types"; const { useParam } = createParam<{ sortBy?: string }>(); export const itemMap = ( - item: WithLoading, -): WithLoading & ComponentProps> => { - if (item.isLoading) return item as any; - - return { - isLoading: item.isLoading, - slug: item.slug, - name: item.name, - subtitle: item.kind !== "collection" ? getDisplayDate(item) : undefined, - href: item.href, - poster: item.poster, - thumbnail: item.thumbnail, - watchStatus: item.kind !== "collection" ? item.watchStatus?.status ?? null : null, - type: item.kind, - watchPercent: item.kind !== "collection" ? item.watchStatus?.watchedPercent ?? null : null, - unseenEpisodesCount: - item.kind === "show" ? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount! : null, - }; -}; + item: LibraryItem, +): ComponentProps & ComponentProps => ({ + slug: item.slug, + name: item.name, + subtitle: item.kind !== "collection" ? getDisplayDate(item) : null, + href: item.href, + poster: item.poster, + thumbnail: item.thumbnail, + watchStatus: item.kind !== "collection" ? item.watchStatus?.status ?? null : null, + type: item.kind, + watchPercent: item.kind !== "collection" ? item.watchStatus?.watchedPercent ?? null : null, + unseenEpisodesCount: + item.kind === "show" ? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount! : null, +}); const query = (sortKey?: SortBy, sortOrd?: SortOrd): QueryIdentifier => ({ parser: LibraryItemP, @@ -92,9 +86,9 @@ export const BrowsePage: QueryPage = () => { setLayout={setLayout} /> } - > - {(item) => } - + Render={({ item }) => } + Loader={() => } + /> ); }; diff --git a/front/packages/ui/src/browse/list.tsx b/front/packages/ui/src/browse/list.tsx index 699d1868..889afacf 100644 --- a/front/packages/ui/src/browse/list.tsx +++ b/front/packages/ui/src/browse/list.tsx @@ -24,6 +24,7 @@ import { ImageBackground, Link, P, + Poster, PosterBackground, Skeleton, imageBorderRadius, @@ -34,8 +35,9 @@ import { useState } from "react"; import { Platform, View } from "react-native"; import { percent, px, rem, useYoshiki } from "yoshiki/native"; import { ItemContext } from "../components/context-menus"; -import type { Layout, WithLoading } from "../fetch"; +import type { Layout } from "../fetch"; import { ItemWatchStatus } from "./grid"; +import { Stylable } from "yoshiki"; export const ItemList = ({ href, @@ -45,22 +47,21 @@ export const ItemList = ({ subtitle, thumbnail, poster, - isLoading, watchStatus, unseenEpisodesCount, ...props -}: WithLoading<{ +}: { href: string; slug: string; type: "movie" | "show" | "collection"; name: string; - subtitle?: string; - poster?: KyooImage | null; - thumbnail?: KyooImage | null; + subtitle: string | null; + poster: KyooImage | null; + thumbnail: KyooImage | null; watchStatus: WatchStatusV | null; unseenEpisodesCount: number | null; -}>) => { - const { css } = useYoshiki(); +}) => { + const { css } = useYoshiki("line"); const [moreOpened, setMoreOpened] = useState(false); return ( @@ -114,25 +115,21 @@ export const ItemList = ({ justifyContent: "center", })} > - - {isLoading || ( - - {name} - - )} - - {slug && watchStatus !== undefined && type && type !== "collection" && ( + + {name} + + {type !== "collection" && ( )} - {(isLoading || subtitle) && ( - - {isLoading || ( -

- {subtitle} -

- )} -
+ {subtitle && ( +

+ {subtitle} +

)} - + ); }; +ItemList.Loader = (props: Stylable) => { + const { css } = useYoshiki(); + + return ( + theme.dark.background, + marginX: ItemList.layout.gap, + }, + props, + )} + > + + + + + + + ); +}; + ItemList.layout = { numColumns: 1, size: 300, layout: "vertical", gap: ts(2) } satisfies Layout; From 2756397898b071a630f26b47ccf84a300950f309 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 20 May 2024 17:54:28 +0200 Subject: [PATCH 3/9] Split loaders in the episode list --- front/packages/ui/src/details/episode.tsx | 128 +++++++++++++--------- front/packages/ui/src/details/season.tsx | 88 ++++++++++----- front/packages/ui/src/details/show.tsx | 3 +- front/packages/ui/src/player/index.tsx | 2 +- front/translations/en.json | 3 +- 5 files changed, 142 insertions(+), 82 deletions(-) diff --git a/front/packages/ui/src/details/episode.tsx b/front/packages/ui/src/details/episode.tsx index d9d0af7b..7b8c9365 100644 --- a/front/packages/ui/src/details/episode.tsx +++ b/front/packages/ui/src/details/episode.tsx @@ -32,6 +32,7 @@ import { important, tooltip, ts, + Image, } from "@kyoo/primitives"; import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg"; import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg"; @@ -43,18 +44,15 @@ import { ItemProgress } from "../browse/grid"; import { EpisodesContext } from "../components/context-menus"; import type { Layout, WithLoading } from "../fetch"; -export const episodeDisplayNumber = ( - episode: { - seasonNumber?: number | null; - episodeNumber?: number | null; - absoluteNumber?: number | null; - }, - def?: string, -) => { +export const episodeDisplayNumber = (episode: { + seasonNumber?: number | null; + episodeNumber?: number | null; + absoluteNumber?: number | null; +}) => { if (typeof episode.seasonNumber === "number" && typeof episode.episodeNumber === "number") return `S${episode.seasonNumber}:E${episode.episodeNumber}`; if (episode.absoluteNumber) return episode.absoluteNumber.toString(); - return def; + return "??"; }; export const displayRuntime = (runtime: number | null) => { @@ -187,7 +185,6 @@ export const EpisodeLine = ({ name, thumbnail, overview, - isLoading, id, absoluteNumber, episodeNumber, @@ -198,7 +195,7 @@ export const EpisodeLine = ({ watchedStatus, href, ...props -}: WithLoading<{ +}: { id: string; slug: string; // if show slug is null, disable "Go to show" in the context menu @@ -215,8 +212,7 @@ export const EpisodeLine = ({ watchedPercent: number | null; watchedStatus: WatchStatusV | null; href: string; -}> & - PressableProps & +} & PressableProps & Stylable) => { const [moreOpened, setMoreOpened] = useState(false); const [descriptionExpanded, setDescriptionExpanded] = useState(false); @@ -254,7 +250,6 @@ export const EpisodeLine = ({ quality="low" alt="" gradient={false} - hideLoad={false} layout={{ width: percent(18), aspectRatio: 16 / 9, @@ -293,48 +288,36 @@ export const EpisodeLine = ({ justifyContent: "space-between", })} > - - {isLoading || ( - // biome-ignore lint/a11y/useValidAriaValues: simply use H6 for the style but keep a P -
- {[displayNumber, name ?? t("show.episodeNoMetadata")].join(" · ")} -
- )} -
+ {/* biome-ignore lint/a11y/useValidAriaValues: simply use H6 for the style but keep a P */} +
+ {[displayNumber, name ?? t("show.episodeNoMetadata")].join(" · ")} +
- - {isLoading || ( - - {/* Source https://www.i18next.com/translation-function/formatting#datetime */} - {[ - releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null, - displayRuntime(runtime), - ] - .filter((item) => item != null) - .join(" · ")} - - )} - - {slug && watchedStatus !== undefined && ( - setMoreOpened(v)} - {...css([ - "more", - { display: "flex", marginLeft: ts(3) }, - Platform.OS === "web" && moreOpened && { display: important("flex") }, - ])} - /> - )} + + {[ + // @ts-ignore Source https://www.i18next.com/translation-function/formatting#datetime + releaseDate ? t("{{val, datetime}}", { val: releaseDate }) : null, + displayRuntime(runtime), + ] + .filter((item) => item != null) + .join(" · ")} + + setMoreOpened(v)} + {...css([ + "more", + { display: "flex", marginLeft: ts(3) }, + Platform.OS === "web" && moreOpened && { display: important("flex") }, + ])} + /> - - - {isLoading ||

{overview}

} -
+ +

{overview}

); }; + +EpisodeLine.Loader = (props: Stylable) => { + const { css } = useYoshiki(); + + return ( + + + + + + + + + + + ); +}; + EpisodeLine.layout = { numColumns: 1, size: 100, diff --git a/front/packages/ui/src/details/season.tsx b/front/packages/ui/src/details/season.tsx index 8150f71e..ab6f09b3 100644 --- a/front/packages/ui/src/details/season.tsx +++ b/front/packages/ui/src/details/season.tsx @@ -26,7 +26,18 @@ import { SeasonP, useInfiniteFetch, } from "@kyoo/models"; -import { H6, HR, IconButton, Menu, P, Skeleton, tooltip, ts, usePageStyle } from "@kyoo/primitives"; +import { + H2, + H6, + HR, + IconButton, + Menu, + P, + Skeleton, + tooltip, + ts, + usePageStyle, +} from "@kyoo/primitives"; import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg"; import type { ComponentType } from "react"; import { useTranslation } from "react-i18next"; @@ -38,14 +49,12 @@ import { EpisodeLine, episodeDisplayNumber } from "./episode"; type SeasonProcessed = Season & { href: string }; export const SeasonHeader = ({ - isLoading, seasonNumber, name, seasons, }: { - isLoading: boolean; - seasonNumber?: number; - name?: string; + seasonNumber: number; + name: string | null; seasons?: SeasonProcessed[]; }) => { const { css } = useYoshiki(); @@ -63,21 +72,20 @@ export const SeasonHeader = ({ fontSize: rem(1.5), })} > - {isLoading ? : seasonNumber} + {seasonNumber}

-
- {isLoading ? : name} -
+

+ {name ?? t("show.season", { number: seasonNumber })} +

{seasons ?.filter((x) => x.episodesCount > 0) .map((x) => ( ))} @@ -88,6 +96,31 @@ export const SeasonHeader = ({ ); }; +SeasonHeader.Loader = () => { + const { css } = useYoshiki(); + + return ( + + + + + + + + +
+
+ ); +}; + SeasonHeader.query = (slug: string): QueryIdentifier => ({ parser: SeasonP, path: ["show", slug, "seasons"], @@ -130,32 +163,37 @@ export const EpisodeList = ({ headerProps={headerProps} getItemType={(item) => (item.firstOfSeason ? "withHeader" : "normal")} contentContainerStyle={pageStyle} - > - {(item) => { + placeholderCount={5} + Render={({ item }) => { const sea = item?.firstOfSeason ? seasons?.find((x) => x.seasonNumber === item.seasonNumber) : null; return ( <> - {item.firstOfSeason && ( - - )} + {item.firstOfSeason && + (sea ? ( + + ) : ( + + ))} ); }} - + Loader={({ index }) => ( + <> + {index === 0 && } + + + )} + /> ); }; diff --git a/front/packages/ui/src/details/show.tsx b/front/packages/ui/src/details/show.tsx index 741e211a..f981efec 100644 --- a/front/packages/ui/src/details/show.tsx +++ b/front/packages/ui/src/details/show.tsx @@ -79,12 +79,11 @@ export const ShowWatchStatusCard = ({ watchedPercent, status, nextEpisode }: Sho >

{t("show.nextUp")}

setFocus(true)} onHoverOut={() => setFocus(false)} onFocus={() => setFocus(true)} diff --git a/front/packages/ui/src/player/index.tsx b/front/packages/ui/src/player/index.tsx index df35889f..5324ffca 100644 --- a/front/packages/ui/src/player/index.tsx +++ b/front/packages/ui/src/player/index.tsx @@ -53,7 +53,7 @@ const mapData = ( if (!data) return { isLoading: true }; return { isLoading: false, - name: data.type === "movie" ? data.name : `${episodeDisplayNumber(data, "")} ${data.name}`, + name: data.type === "movie" ? data.name : `${episodeDisplayNumber(data)} ${data.name}`, showName: data.type === "movie" ? data.name! : data.show!.name, poster: data.type === "movie" ? data.poster : data.show!.poster, subtitles: info?.subtitles, diff --git a/front/translations/en.json b/front/translations/en.json index bd0ee0f4..fd659f8f 100644 --- a/front/translations/en.json +++ b/front/translations/en.json @@ -39,7 +39,8 @@ "droped": "Mark as dropped", "null": "Mark as not seen" }, - "nextUp": "Next up" + "nextUp": "Next up", + "season": "Season {{number}}" }, "browse": { "sortby": "Sort by {{key}}", From 393c58b10aa45f7c40843422fe80e6ff8839c465 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 20 May 2024 19:36:23 +0200 Subject: [PATCH 4/9] Split loaders for most items on the main page --- front/packages/ui/src/browse/grid.tsx | 2 +- front/packages/ui/src/browse/index.tsx | 2 +- front/packages/ui/src/browse/list.tsx | 4 +- front/packages/ui/src/details/episode.tsx | 122 ++++++++++--------- front/packages/ui/src/details/season.tsx | 13 +- front/packages/ui/src/fetch-infinite.tsx | 2 +- front/packages/ui/src/fetch-infinite.web.tsx | 2 +- front/packages/ui/src/home/genre.tsx | 10 +- front/packages/ui/src/home/news.tsx | 60 ++++----- front/packages/ui/src/home/vertical.tsx | 6 +- front/packages/ui/src/home/watchlist.tsx | 108 ++++++++-------- 11 files changed, 169 insertions(+), 162 deletions(-) diff --git a/front/packages/ui/src/browse/grid.tsx b/front/packages/ui/src/browse/grid.tsx index 10c23a9d..ad5e7326 100644 --- a/front/packages/ui/src/browse/grid.tsx +++ b/front/packages/ui/src/browse/grid.tsx @@ -213,7 +213,7 @@ export const ItemGrid = ({ ); }; -ItemGrid.Loader = (props: Stylable) => { +ItemGrid.Loader = (props: object) => { const { css } = useYoshiki(); return ( diff --git a/front/packages/ui/src/browse/index.tsx b/front/packages/ui/src/browse/index.tsx index 9863657e..be1be7c9 100644 --- a/front/packages/ui/src/browse/index.tsx +++ b/front/packages/ui/src/browse/index.tsx @@ -87,7 +87,7 @@ export const BrowsePage: QueryPage = () => { /> } Render={({ item }) => } - Loader={() => } + Loader={LayoutComponent.Loader} /> ); }; diff --git a/front/packages/ui/src/browse/list.tsx b/front/packages/ui/src/browse/list.tsx index 889afacf..d2f50704 100644 --- a/front/packages/ui/src/browse/list.tsx +++ b/front/packages/ui/src/browse/list.tsx @@ -37,7 +37,7 @@ import { percent, px, rem, useYoshiki } from "yoshiki/native"; import { ItemContext } from "../components/context-menus"; import type { Layout } from "../fetch"; import { ItemWatchStatus } from "./grid"; -import { Stylable } from "yoshiki"; +import type { Stylable } from "yoshiki"; export const ItemList = ({ href, @@ -166,7 +166,7 @@ export const ItemList = ({ ); }; -ItemList.Loader = (props: Stylable) => { +ItemList.Loader = (props: object) => { const { css } = useYoshiki(); return ( diff --git a/front/packages/ui/src/details/episode.tsx b/front/packages/ui/src/details/episode.tsx index 7b8c9365..f35c25a7 100644 --- a/front/packages/ui/src/details/episode.tsx +++ b/front/packages/ui/src/details/episode.tsx @@ -42,7 +42,7 @@ import { type ImageStyle, Platform, type PressableProps, View } from "react-nati import { type Stylable, type Theme, percent, rem, useYoshiki } from "yoshiki/native"; import { ItemProgress } from "../browse/grid"; import { EpisodesContext } from "../components/context-menus"; -import type { Layout, WithLoading } from "../fetch"; +import type { Layout } from "../fetch"; export const episodeDisplayNumber = (episode: { seasonNumber?: number | null; @@ -67,23 +67,21 @@ export const EpisodeBox = ({ name, overview, thumbnail, - isLoading, href, watchedPercent, watchedStatus, ...props -}: Stylable & - WithLoading<{ - slug: string; - // if show slug is null, disable "Go to show" in the context menu - showSlug: string | null; - name: string | null; - overview: string | null; - href: string; - thumbnail?: ImageProps["src"] | null; - watchedPercent: number | null; - watchedStatus: WatchStatusV | null; - }>) => { +}: Stylable & { + slug: string; + // if show slug is null, disable "Go to show" in the context menu + showSlug: string | null; + name: string | null; + overview: string | null; + href: string; + thumbnail?: ImageProps["src"] | null; + watchedPercent: number | null; + watchedStatus: WatchStatusV | null; +}) => { const [moreOpened, setMoreOpened] = useState(false); const { css } = useYoshiki("episodebox"); const { t } = useTranslation(); @@ -126,58 +124,72 @@ export const EpisodeBox = ({ quality="low" alt="" gradient={false} - hideLoad={false} - forcedLoading={isLoading} layout={{ width: percent(100), aspectRatio: 16 / 9 }} {...(css("poster") as any)} > {(watchedPercent || watchedStatus === WatchStatusV.Completed) && ( )} - {slug && watchedStatus !== undefined && ( - setMoreOpened(v)} - {...css([ - { - position: "absolute", - top: 0, - right: 0, - bg: (theme) => theme.darkOverlay, - }, - "more", - Platform.OS === "web" && moreOpened && { display: important("flex") }, - ])} - /> - )} + setMoreOpened(v)} + {...css([ + { + position: "absolute", + top: 0, + right: 0, + bg: (theme) => theme.darkOverlay, + }, + "more", + Platform.OS === "web" && moreOpened && { display: important("flex") }, + ])} + /> - - {isLoading || ( -

- {name ?? t("show.episodeNoMetadata")} -

- )} -
- - {isLoading || ( - - {overview} - - )} - +

+ {name ?? t("show.episodeNoMetadata")} +

+ + {overview} + ); }; +EpisodeBox.Loader = (props: Stylable) => { + const { css } = useYoshiki(); + + return ( + + theme.background, + borderWidth: ts(0.5), + borderStyle: "solid", + })} + /> + + + + ); +}; + export const EpisodeLine = ({ slug, showSlug, diff --git a/front/packages/ui/src/details/season.tsx b/front/packages/ui/src/details/season.tsx index ab6f09b3..7f1a1876 100644 --- a/front/packages/ui/src/details/season.tsx +++ b/front/packages/ui/src/details/season.tsx @@ -26,18 +26,7 @@ import { SeasonP, useInfiniteFetch, } from "@kyoo/models"; -import { - H2, - H6, - HR, - IconButton, - Menu, - P, - Skeleton, - tooltip, - ts, - usePageStyle, -} from "@kyoo/primitives"; +import { H2, HR, IconButton, Menu, P, Skeleton, tooltip, ts, usePageStyle } from "@kyoo/primitives"; import MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg"; import type { ComponentType } from "react"; import { useTranslation } from "react-i18next"; diff --git a/front/packages/ui/src/fetch-infinite.tsx b/front/packages/ui/src/fetch-infinite.tsx index 1ff4231e..d3295798 100644 --- a/front/packages/ui/src/fetch-infinite.tsx +++ b/front/packages/ui/src/fetch-infinite.tsx @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -import { type Page, type QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; +import { type QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { HR, useBreakpointMap } from "@kyoo/primitives"; import { type ContentStyle, FlashList } from "@shopify/flash-list"; import { diff --git a/front/packages/ui/src/fetch-infinite.web.tsx b/front/packages/ui/src/fetch-infinite.web.tsx index 61c33051..53186008 100644 --- a/front/packages/ui/src/fetch-infinite.web.tsx +++ b/front/packages/ui/src/fetch-infinite.web.tsx @@ -18,7 +18,7 @@ * along with Kyoo. If not, see . */ -import { type Page, type QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; +import { type QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { HR } from "@kyoo/primitives"; import type { ContentStyle } from "@shopify/flash-list"; import { diff --git a/front/packages/ui/src/home/genre.tsx b/front/packages/ui/src/home/genre.tsx index 4deba79c..5f580dc3 100644 --- a/front/packages/ui/src/home/genre.tsx +++ b/front/packages/ui/src/home/genre.tsx @@ -75,13 +75,9 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => { layout={{ ...ItemGrid.layout, layout: "horizontal" }} placeholderCount={2} empty={displayEmpty.current ? t("home.none") : undefined} - > - {(x, i) => { - // only display empty list if a loading as been displayed (not durring ssr) - if (x.isLoading) displayEmpty.current = true; - return ; - }} - + Render={({ item }) => } + Loader={ItemGrid.Loader} + /> ); }; diff --git a/front/packages/ui/src/home/news.tsx b/front/packages/ui/src/home/news.tsx index bb8295e9..3c4ec8aa 100644 --- a/front/packages/ui/src/home/news.tsx +++ b/front/packages/ui/src/home/news.tsx @@ -39,38 +39,40 @@ export const NewsList = () => { getItemType={(x, i) => (x.kind === "movie" || (x.isLoading && i % 2) ? "movie" : "episode")} getItemSize={(kind) => (kind === "episode" ? 2 : 1)} empty={t("home.none")} - > - {(x, i) => - x.kind === "movie" || (x.isLoading && i % 2) ? ( + Render={({ item }) => { + if (item.kind === "episode") { + return ( + + ); + } + return ( - ) : ( - - ) - } - + ); + }} + Loader={({ index }) => (index % 2 ? : )} + /> ); }; diff --git a/front/packages/ui/src/home/vertical.tsx b/front/packages/ui/src/home/vertical.tsx index 813734ca..246ebe26 100644 --- a/front/packages/ui/src/home/vertical.tsx +++ b/front/packages/ui/src/home/vertical.tsx @@ -41,9 +41,9 @@ export const VerticalRecommended = () => { layout={{ ...ItemList.layout, layout: "vertical" }} fetchMore={false} nested - > - {(x, i) => } - + Render={({ item }) => } + Loader={() => } + /> ); }; diff --git a/front/packages/ui/src/home/watchlist.tsx b/front/packages/ui/src/home/watchlist.tsx index fa2e695e..460f906f 100644 --- a/front/packages/ui/src/home/watchlist.tsx +++ b/front/packages/ui/src/home/watchlist.tsx @@ -39,55 +39,10 @@ export const WatchlistList = () => { const { css } = useYoshiki(); const account = useAccount(); - return ( - <> -
- {account ? ( - - (x.kind === "show" && x.watchStatus?.nextEpisode) || (x.isLoading && i % 2) - ? "episode" - : "item" - } - getItemSize={(kind) => (kind === "episode" ? 2 : 1)} - empty={t("home.none")} - > - {(x, i) => { - const episode = x.kind === "show" ? x.watchStatus?.nextEpisode : null; - return (x.kind === "show" && x.watchStatus?.nextEpisode) || (x.isLoading && i % 2) ? ( - - ) : ( - - ); - }} - - ) : ( + if (!account) { + return ( + <> +

{t("home.watchlistLogin")}