Split loaders for most items on the main page

This commit is contained in:
Zoe Roux 2024-05-20 19:36:23 +02:00
parent 2756397898
commit 393c58b10a
No known key found for this signature in database
11 changed files with 169 additions and 162 deletions

View File

@ -213,7 +213,7 @@ export const ItemGrid = ({
); );
}; };
ItemGrid.Loader = (props: Stylable) => { ItemGrid.Loader = (props: object) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
return ( return (

View File

@ -87,7 +87,7 @@ export const BrowsePage: QueryPage = () => {
/> />
} }
Render={({ item }) => <LayoutComponent {...itemMap(item)} />} Render={({ item }) => <LayoutComponent {...itemMap(item)} />}
Loader={() => <LayoutComponent.Loader />} Loader={LayoutComponent.Loader}
/> />
); );
}; };

View File

@ -37,7 +37,7 @@ import { percent, px, rem, useYoshiki } from "yoshiki/native";
import { ItemContext } from "../components/context-menus"; import { ItemContext } from "../components/context-menus";
import type { Layout } from "../fetch"; import type { Layout } from "../fetch";
import { ItemWatchStatus } from "./grid"; import { ItemWatchStatus } from "./grid";
import { Stylable } from "yoshiki"; import type { Stylable } from "yoshiki";
export const ItemList = ({ export const ItemList = ({
href, href,
@ -166,7 +166,7 @@ export const ItemList = ({
); );
}; };
ItemList.Loader = (props: Stylable) => { ItemList.Loader = (props: object) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
return ( return (

View File

@ -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 { type Stylable, type Theme, percent, rem, useYoshiki } from "yoshiki/native";
import { ItemProgress } from "../browse/grid"; import { ItemProgress } from "../browse/grid";
import { EpisodesContext } from "../components/context-menus"; import { EpisodesContext } from "../components/context-menus";
import type { Layout, WithLoading } from "../fetch"; import type { Layout } from "../fetch";
export const episodeDisplayNumber = (episode: { export const episodeDisplayNumber = (episode: {
seasonNumber?: number | null; seasonNumber?: number | null;
@ -67,23 +67,21 @@ export const EpisodeBox = ({
name, name,
overview, overview,
thumbnail, thumbnail,
isLoading,
href, href,
watchedPercent, watchedPercent,
watchedStatus, watchedStatus,
...props ...props
}: Stylable & }: Stylable & {
WithLoading<{ slug: string;
slug: string; // if show slug is null, disable "Go to show" in the context menu
// if show slug is null, disable "Go to show" in the context menu showSlug: string | null;
showSlug: string | null; name: string | null;
name: string | null; overview: string | null;
overview: string | null; href: string;
href: string; thumbnail?: ImageProps["src"] | null;
thumbnail?: ImageProps["src"] | null; watchedPercent: number | null;
watchedPercent: number | null; watchedStatus: WatchStatusV | null;
watchedStatus: WatchStatusV | null; }) => {
}>) => {
const [moreOpened, setMoreOpened] = useState(false); const [moreOpened, setMoreOpened] = useState(false);
const { css } = useYoshiki("episodebox"); const { css } = useYoshiki("episodebox");
const { t } = useTranslation(); const { t } = useTranslation();
@ -126,58 +124,72 @@ export const EpisodeBox = ({
quality="low" quality="low"
alt="" alt=""
gradient={false} gradient={false}
hideLoad={false}
forcedLoading={isLoading}
layout={{ width: percent(100), aspectRatio: 16 / 9 }} layout={{ width: percent(100), aspectRatio: 16 / 9 }}
{...(css("poster") as any)} {...(css("poster") as any)}
> >
{(watchedPercent || watchedStatus === WatchStatusV.Completed) && ( {(watchedPercent || watchedStatus === WatchStatusV.Completed) && (
<ItemProgress watchPercent={watchedPercent ?? 100} /> <ItemProgress watchPercent={watchedPercent ?? 100} />
)} )}
{slug && watchedStatus !== undefined && ( <EpisodesContext
<EpisodesContext slug={slug}
slug={slug} showSlug={showSlug}
showSlug={showSlug} status={watchedStatus}
status={watchedStatus} isOpen={moreOpened}
isOpen={moreOpened} setOpen={(v) => setMoreOpened(v)}
setOpen={(v) => setMoreOpened(v)} {...css([
{...css([ {
{ position: "absolute",
position: "absolute", top: 0,
top: 0, right: 0,
right: 0, bg: (theme) => theme.darkOverlay,
bg: (theme) => theme.darkOverlay, },
}, "more",
"more", Platform.OS === "web" && moreOpened && { display: important("flex") },
Platform.OS === "web" && moreOpened && { display: important("flex") }, ])}
])} />
/>
)}
</ImageBackground> </ImageBackground>
<Skeleton {...css({ width: percent(50) })}> <P {...css([{ marginY: 0, textAlign: "center" }, "title"])}>
{isLoading || ( {name ?? t("show.episodeNoMetadata")}
<P {...css([{ marginY: 0, textAlign: "center" }, "title"])}> </P>
{name ?? t("show.episodeNoMetadata")} <SubP
</P> numberOfLines={3}
)} {...css({
</Skeleton> marginTop: 0,
<Skeleton {...css({ width: percent(75), height: rem(0.8) })}> textAlign: "center",
{isLoading || ( })}
<SubP >
numberOfLines={3} {overview}
{...css({ </SubP>
marginTop: 0,
textAlign: "center",
})}
>
{overview}
</SubP>
)}
</Skeleton>
</Link> </Link>
); );
}; };
EpisodeBox.Loader = (props: Stylable) => {
const { css } = useYoshiki();
return (
<View
{...css(
{
alignItems: "center",
},
props,
)}
>
<Image.Loader
layout={{ width: percent(100), aspectRatio: 16 / 9 }}
{...css({
borderColor: (theme) => theme.background,
borderWidth: ts(0.5),
borderStyle: "solid",
})}
/>
<Skeleton {...css({ width: percent(50) })} />
<Skeleton {...css({ width: percent(75), height: rem(0.8) })} />
</View>
);
};
export const EpisodeLine = ({ export const EpisodeLine = ({
slug, slug,
showSlug, showSlug,

View File

@ -26,18 +26,7 @@ import {
SeasonP, SeasonP,
useInfiniteFetch, useInfiniteFetch,
} from "@kyoo/models"; } from "@kyoo/models";
import { import { H2, HR, IconButton, Menu, P, Skeleton, tooltip, ts, usePageStyle } from "@kyoo/primitives";
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 MenuIcon from "@material-symbols/svg-400/rounded/menu-fill.svg";
import type { ComponentType } from "react"; import type { ComponentType } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { type Page, type QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { type QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { HR, useBreakpointMap } from "@kyoo/primitives"; import { HR, useBreakpointMap } from "@kyoo/primitives";
import { type ContentStyle, FlashList } from "@shopify/flash-list"; import { type ContentStyle, FlashList } from "@shopify/flash-list";
import { import {

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { type Page, type QueryIdentifier, useInfiniteFetch } from "@kyoo/models"; import { type QueryIdentifier, useInfiniteFetch } from "@kyoo/models";
import { HR } from "@kyoo/primitives"; import { HR } from "@kyoo/primitives";
import type { ContentStyle } from "@shopify/flash-list"; import type { ContentStyle } from "@shopify/flash-list";
import { import {

View File

@ -75,13 +75,9 @@ export const GenreGrid = ({ genre }: { genre: Genre }) => {
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}
> Render={({ item }) => <ItemGrid {...itemMap(item)} />}
{(x, i) => { Loader={ItemGrid.Loader}
// only display empty list if a loading as been displayed (not durring ssr) />
if (x.isLoading) displayEmpty.current = true;
return <ItemGrid key={x.id ?? i} {...itemMap(x)} />;
}}
</InfiniteFetchList>
</> </>
); );
}; };

View File

@ -39,38 +39,40 @@ export const NewsList = () => {
getItemType={(x, i) => (x.kind === "movie" || (x.isLoading && i % 2) ? "movie" : "episode")} getItemType={(x, i) => (x.kind === "movie" || (x.isLoading && i % 2) ? "movie" : "episode")}
getItemSize={(kind) => (kind === "episode" ? 2 : 1)} getItemSize={(kind) => (kind === "episode" ? 2 : 1)}
empty={t("home.none")} empty={t("home.none")}
> Render={({ item }) => {
{(x, i) => if (item.kind === "episode") {
x.kind === "movie" || (x.isLoading && i % 2) ? ( return (
<EpisodeBox
slug={item.slug}
showSlug={item.show!.slug}
name={`${item.show!.name} ${episodeDisplayNumber(item)}`}
overview={item.name}
thumbnail={item.thumbnail}
href={item.href}
watchedPercent={item.watchStatus?.watchedPercent || null}
watchedStatus={item.watchStatus?.status || null}
// TODO: Move this into the ItemList (using getItemSize)
// @ts-expect-error This is a web only property
{...css({ gridColumnEnd: "span 2" })}
/>
);
}
return (
<ItemGrid <ItemGrid
isLoading={x.isLoading as any} href={item.href}
href={x.href} slug={item.slug}
slug={x.slug} name={item.name!}
name={x.name!} subtitle={getDisplayDate(item)}
subtitle={!x.isLoading ? getDisplayDate(x) : undefined} poster={item.poster}
poster={x.poster} watchStatus={item.watchStatus?.status || null}
watchStatus={x.watchStatus?.status || null} watchPercent={item.watchStatus?.watchedPercent || null}
watchPercent={x.watchStatus?.watchedPercent || null} unseenEpisodesCount={null}
type={"movie"} type={"movie"}
/> />
) : ( );
<EpisodeBox }}
isLoading={x.isLoading as any} Loader={({ index }) => (index % 2 ? <EpisodeBox.Loader /> : <ItemGrid.Loader />)}
slug={x.slug} />
showSlug={x.kind === "episode" ? x.show!.slug : null}
name={x.kind === "episode" ? `${x.show!.name} ${episodeDisplayNumber(x)}` : undefined}
overview={x.name}
thumbnail={x.thumbnail}
href={x.href}
watchedPercent={x.watchStatus?.watchedPercent || null}
watchedStatus={x.watchStatus?.status || null}
// TODO: Move this into the ItemList (using getItemSize)
// @ts-expect-error This is a web only property
{...css({ gridColumnEnd: "span 2" })}
/>
)
}
</InfiniteFetch>
</> </>
); );
}; };

View File

@ -41,9 +41,9 @@ export const VerticalRecommended = () => {
layout={{ ...ItemList.layout, layout: "vertical" }} layout={{ ...ItemList.layout, layout: "vertical" }}
fetchMore={false} fetchMore={false}
nested nested
> Render={({ item }) => <ItemList {...itemMap(item)} />}
{(x, i) => <ItemList key={x.id ?? i} {...itemMap(x)} />} Loader={() => <ItemList.Loader />}
</InfiniteFetch> />
</View> </View>
); );
}; };

View File

@ -39,55 +39,10 @@ export const WatchlistList = () => {
const { css } = useYoshiki(); const { css } = useYoshiki();
const account = useAccount(); const account = useAccount();
return ( if (!account) {
<> return (
<Header title={t("home.watchlist")} /> <>
{account ? ( <Header title={t("home.watchlist")} />
<InfiniteFetch
query={WatchlistList.query()}
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
getItemType={(x, i) =>
(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) ? (
<EpisodeBox
isLoading={x.isLoading as any}
slug={episode?.slug}
showSlug={x.slug}
name={episode ? `${x.name} ${episodeDisplayNumber(episode)}` : undefined}
overview={episode?.name}
thumbnail={episode?.thumbnail ?? x.thumbnail}
href={episode?.href}
watchedPercent={x.watchStatus?.watchedPercent || null}
watchedStatus={x.watchStatus?.status || null}
// TODO: Move this into the ItemList (using getItemSize)
// @ts-expect-error This is a web only property
{...css({ gridColumnEnd: "span 2" })}
/>
) : (
<ItemGrid
isLoading={x.isLoading as any}
href={x.href}
slug={x.slug}
name={x.name!}
subtitle={!x.isLoading ? getDisplayDate(x) : undefined}
poster={x.poster}
watchStatus={x.watchStatus?.status || null}
watchPercent={x.watchStatus?.watchedPercent || null}
unseenEpisodesCount={x.kind === "show" ? x.watchStatus?.unseenEpisodesCount : null}
type={x.kind}
/>
);
}}
</InfiniteFetch>
) : (
<View {...css({ justifyContent: "center", alignItems: "center" })}> <View {...css({ justifyContent: "center", alignItems: "center" })}>
<P>{t("home.watchlistLogin")}</P> <P>{t("home.watchlistLogin")}</P>
<Button <Button
@ -96,7 +51,60 @@ export const WatchlistList = () => {
{...css({ minWidth: ts(24), margin: ts(2) })} {...css({ minWidth: ts(24), margin: ts(2) })}
/> />
</View> </View>
)} </>
);
}
return (
<>
<Header title={t("home.watchlist")} />
<InfiniteFetch
query={WatchlistList.query()}
layout={{ ...ItemGrid.layout, layout: "horizontal" }}
getItemType={(x, i) =>
(x.kind === "show" && x.watchStatus?.nextEpisode) || (x.isLoading && 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) {
return (
<EpisodeBox
slug={episode.slug}
showSlug={item.slug}
name={`${item.name} ${episodeDisplayNumber(episode)}`}
overview={episode.name}
thumbnail={episode.thumbnail ?? item.thumbnail}
href={episode.href}
watchedPercent={item.watchStatus?.watchedPercent || null}
watchedStatus={item.watchStatus?.status || null}
// TODO: Move this into the ItemList (using getItemSize)
// @ts-expect-error This is a web only property
{...css({ gridColumnEnd: "span 2" })}
/>
);
}
return (
<ItemGrid
href={item.href}
slug={item.slug}
name={item.name!}
subtitle={getDisplayDate(item)}
poster={item.poster}
watchStatus={item.watchStatus?.status || null}
watchPercent={item.watchStatus?.watchedPercent || null}
unseenEpisodesCount={
(item.kind === "show" && item.watchStatus?.unseenEpisodesCount) || null
}
type={item.kind}
/>
);
}}
Loader={({ index }) => (index % 2 ? <EpisodeBox.Loader /> : <ItemGrid.Loader />)}
/>
</> </>
); );
}; };