Split loaders in the browse page

This commit is contained in:
Zoe Roux 2024-05-20 17:54:13 +02:00
parent 9f8b2da76e
commit 444de0af26
No known key found for this signature in database
7 changed files with 154 additions and 102 deletions

View File

@ -42,6 +42,7 @@ export const getDisplayDate = (data: Show | Movie) => {
if (airDate) { if (airDate) {
return airDate.getFullYear().toString(); return airDate.getFullYear().toString();
} }
return null;
}; };
export const useLocalSetting = (setting: string, def: string) => { export const useLocalSetting = (setting: string, def: string) => {

View File

@ -93,3 +93,10 @@ export const Image = ({
</View> </View>
); );
}; };
Image.Loader = ({ layout, ...props }: { layout: ImageLayout }) => {
const { css } = useYoshiki();
const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle;
return <Skeleton variant="custom" {...css([layout, border], props)} />;
};

View File

@ -73,3 +73,10 @@ export const Image = ({
</BlurhashContainer> </BlurhashContainer>
); );
}; };
Image.Loader = ({ layout, ...props }: { layout: ImageLayout }) => {
const { css } = useYoshiki();
const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle;
return <Skeleton variant="custom" {...css([layout, border], props)} />;
};

View File

@ -39,6 +39,13 @@ export const Poster = ({
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>; layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
}) => <Image alt={alt!} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />; }) => <Image alt={alt!} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />;
Poster.Loader = ({
layout,
...props
}: {
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
}) => <Image.Loader layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />;
export const PosterBackground = ({ export const PosterBackground = ({
alt, alt,
layout, layout,

View File

@ -23,6 +23,7 @@ import {
Icon, Icon,
Link, Link,
P, P,
Poster,
PosterBackground, PosterBackground,
Skeleton, Skeleton,
SubP, SubP,
@ -35,7 +36,7 @@ import { useState } from "react";
import { type ImageStyle, Platform, View } from "react-native"; import { type ImageStyle, Platform, View } from "react-native";
import { type Stylable, type Theme, max, percent, px, rem, useYoshiki } from "yoshiki/native"; import { type Stylable, type Theme, max, percent, px, rem, useYoshiki } from "yoshiki/native";
import { ItemContext } from "../components/context-menus"; import { ItemContext } from "../components/context-menus";
import type { Layout, WithLoading } from "../fetch"; import type { Layout } from "../fetch";
export const ItemWatchStatus = ({ export const ItemWatchStatus = ({
watchStatus, watchStatus,
@ -113,23 +114,21 @@ export const ItemGrid = ({
type, type,
subtitle, subtitle,
poster, poster,
isLoading,
watchStatus, watchStatus,
watchPercent, watchPercent,
unseenEpisodesCount, unseenEpisodesCount,
...props ...props
}: WithLoading<{ }: {
href: string; href: string;
slug: string; slug: string;
name: string; name: string;
subtitle?: string; subtitle: string | null;
poster?: KyooImage | null; poster: KyooImage | null;
watchStatus: WatchStatusV | null; watchStatus: WatchStatusV | null;
watchPercent: number | null; watchPercent: number | null;
type: "movie" | "show" | "collection"; type: "movie" | "show" | "collection";
unseenEpisodesCount: number | null; unseenEpisodesCount: number | null;
}> & } & Stylable<"text">) => {
Stylable<"text">) => {
const [moreOpened, setMoreOpened] = useState(false); const [moreOpened, setMoreOpened] = useState(false);
const { css } = useYoshiki("grid"); const { css } = useYoshiki("grid");
@ -172,13 +171,12 @@ export const ItemGrid = ({
src={poster} src={poster}
alt={name} alt={name}
quality="low" quality="low"
forcedLoading={isLoading}
layout={{ width: percent(100) }} layout={{ width: percent(100) }}
{...(css("poster") as { style: ImageStyle })} {...(css("poster") as { style: ImageStyle })}
> >
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} /> <ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
{type === "movie" && watchPercent && <ItemProgress watchPercent={watchPercent} />} {type === "movie" && watchPercent && <ItemProgress watchPercent={watchPercent} />}
{slug && watchStatus !== undefined && type && type !== "collection" && ( {type !== "collection" && (
<ItemContext <ItemContext
type={type} type={type}
slug={slug} slug={slug}
@ -198,34 +196,51 @@ export const ItemGrid = ({
/> />
)} )}
</PosterBackground> </PosterBackground>
<Skeleton> <P numberOfLines={subtitle ? 1 : 2} {...css([{ marginY: 0, textAlign: "center" }, "title"])}>
{isLoading || ( {name}
<P </P>
numberOfLines={subtitle ? 1 : 2} {subtitle && (
{...css([{ marginY: 0, textAlign: "center" }, "title"])} <SubP
> {...css({
{name} marginTop: 0,
</P> textAlign: "center",
)} })}
</Skeleton> >
{(isLoading || subtitle) && ( {subtitle}
<Skeleton {...css({ width: percent(50) })}> </SubP>
{isLoading || (
<SubP
{...css({
marginTop: 0,
textAlign: "center",
})}
>
{subtitle}
</SubP>
)}
</Skeleton>
)} )}
</Link> </Link>
); );
}; };
ItemGrid.Loader = (props: Stylable) => {
const { css } = useYoshiki();
return (
<View
{...css(
{
flexDirection: "column",
alignItems: "center",
width: percent(100),
},
props,
)}
>
<Poster.Loader
layout={{ width: percent(100) }}
{...css({
borderColor: (theme) => theme.background,
borderWidth: ts(0.5),
borderStyle: "solid",
})}
/>
<Skeleton />
<Skeleton {...css({ width: percent(50) })} />
</View>
);
};
ItemGrid.layout = { ItemGrid.layout = {
size: px(150), size: px(150),
numColumns: { xs: 3, sm: 4, md: 5, lg: 6, xl: 8 }, numColumns: { xs: 3, sm: 4, md: 5, lg: 6, xl: 8 },

View File

@ -27,7 +27,6 @@ import {
} from "@kyoo/models"; } from "@kyoo/models";
import { type ComponentProps, useState } from "react"; import { type ComponentProps, useState } from "react";
import { createParam } from "solito"; import { createParam } from "solito";
import type { WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite"; import { InfiniteFetch } from "../fetch-infinite";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { ItemGrid } from "./grid"; import { ItemGrid } from "./grid";
@ -38,25 +37,20 @@ import { Layout, SortBy, SortOrd } from "./types";
const { useParam } = createParam<{ sortBy?: string }>(); const { useParam } = createParam<{ sortBy?: string }>();
export const itemMap = ( export const itemMap = (
item: WithLoading<LibraryItem>, item: LibraryItem,
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => { ): ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList> => ({
if (item.isLoading) return item as any; slug: item.slug,
name: item.name,
return { subtitle: item.kind !== "collection" ? getDisplayDate(item) : null,
isLoading: item.isLoading, href: item.href,
slug: item.slug, poster: item.poster,
name: item.name, thumbnail: item.thumbnail,
subtitle: item.kind !== "collection" ? getDisplayDate(item) : undefined, watchStatus: item.kind !== "collection" ? item.watchStatus?.status ?? null : null,
href: item.href, type: item.kind,
poster: item.poster, watchPercent: item.kind !== "collection" ? item.watchStatus?.watchedPercent ?? null : null,
thumbnail: item.thumbnail, unseenEpisodesCount:
watchStatus: item.kind !== "collection" ? item.watchStatus?.status ?? null : null, item.kind === "show" ? item.watchStatus?.unseenEpisodesCount ?? item.episodesCount! : 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<LibraryItem> => ({ const query = (sortKey?: SortBy, sortOrd?: SortOrd): QueryIdentifier<LibraryItem> => ({
parser: LibraryItemP, parser: LibraryItemP,
@ -92,9 +86,9 @@ export const BrowsePage: QueryPage = () => {
setLayout={setLayout} setLayout={setLayout}
/> />
} }
> Render={({ item }) => <LayoutComponent {...itemMap(item)} />}
{(item) => <LayoutComponent {...itemMap(item)} />} Loader={() => <LayoutComponent.Loader />}
</InfiniteFetch> />
); );
}; };

View File

@ -24,6 +24,7 @@ import {
ImageBackground, ImageBackground,
Link, Link,
P, P,
Poster,
PosterBackground, PosterBackground,
Skeleton, Skeleton,
imageBorderRadius, imageBorderRadius,
@ -34,8 +35,9 @@ import { useState } from "react";
import { Platform, View } from "react-native"; import { Platform, View } from "react-native";
import { percent, px, rem, useYoshiki } from "yoshiki/native"; import { percent, px, rem, useYoshiki } from "yoshiki/native";
import { ItemContext } from "../components/context-menus"; import { ItemContext } from "../components/context-menus";
import type { Layout, WithLoading } from "../fetch"; import type { Layout } from "../fetch";
import { ItemWatchStatus } from "./grid"; import { ItemWatchStatus } from "./grid";
import { Stylable } from "yoshiki";
export const ItemList = ({ export const ItemList = ({
href, href,
@ -45,22 +47,21 @@ export const ItemList = ({
subtitle, subtitle,
thumbnail, thumbnail,
poster, poster,
isLoading,
watchStatus, watchStatus,
unseenEpisodesCount, unseenEpisodesCount,
...props ...props
}: WithLoading<{ }: {
href: string; href: string;
slug: string; slug: string;
type: "movie" | "show" | "collection"; type: "movie" | "show" | "collection";
name: string; name: string;
subtitle?: string; subtitle: string | null;
poster?: KyooImage | null; poster: KyooImage | null;
thumbnail?: KyooImage | null; thumbnail: KyooImage | null;
watchStatus: WatchStatusV | null; watchStatus: WatchStatusV | null;
unseenEpisodesCount: number | null; unseenEpisodesCount: number | null;
}>) => { }) => {
const { css } = useYoshiki(); const { css } = useYoshiki("line");
const [moreOpened, setMoreOpened] = useState(false); const [moreOpened, setMoreOpened] = useState(false);
return ( return (
@ -114,25 +115,21 @@ export const ItemList = ({
justifyContent: "center", justifyContent: "center",
})} })}
> >
<Skeleton {...css({ height: rem(2), alignSelf: "center" })}> <Heading
{isLoading || ( {...css([
<Heading "title",
{...css([ {
"title", textAlign: "center",
{ fontSize: rem(2),
textAlign: "center", letterSpacing: rem(0.002),
fontSize: rem(2), fontWeight: "900",
letterSpacing: rem(0.002), textTransform: "uppercase",
fontWeight: "900", },
textTransform: "uppercase", ])}
}, >
])} {name}
> </Heading>
{name} {type !== "collection" && (
</Heading>
)}
</Skeleton>
{slug && watchStatus !== undefined && type && type !== "collection" && (
<ItemContext <ItemContext
type={type} type={type}
slug={slug} slug={slug}
@ -151,32 +148,56 @@ export const ItemList = ({
/> />
)} )}
</View> </View>
{(isLoading || subtitle) && ( {subtitle && (
<Skeleton {...css({ width: rem(5), alignSelf: "center" })}> <P
{isLoading || ( {...css({
<P textAlign: "center",
{...css({ marginRight: ts(4),
textAlign: "center", })}
marginRight: ts(4), >
})} {subtitle}
> </P>
{subtitle}
</P>
)}
</Skeleton>
)} )}
</View> </View>
<PosterBackground <PosterBackground src={poster} alt="" quality="low" layout={{ height: percent(80) }}>
src={poster}
alt=""
quality="low"
forcedLoading={isLoading}
layout={{ height: percent(80) }}
>
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} /> <ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
</PosterBackground> </PosterBackground>
</ImageBackground> </ImageBackground>
); );
}; };
ItemList.Loader = (props: Stylable) => {
const { css } = useYoshiki();
return (
<View
{...css(
{
alignItems: "center",
justifyContent: "space-evenly",
flexDirection: "row",
height: ItemList.layout.size,
borderRadius: px(imageBorderRadius),
overflow: "hidden",
bg: (theme) => theme.dark.background,
marginX: ItemList.layout.gap,
},
props,
)}
>
<View
{...css({
width: { xs: "50%", lg: "30%" },
flexDirection: "column",
justifyContent: "center",
})}
>
<Skeleton {...css({ height: rem(2), alignSelf: "center" })} />
<Skeleton {...css({ width: rem(5), alignSelf: "center" })} />
</View>
<Poster.Loader layout={{ height: percent(80) }} />
</View>
);
};
ItemList.layout = { numColumns: 1, size: 300, layout: "vertical", gap: ts(2) } satisfies Layout; ItemList.layout = { numColumns: 1, size: 300, layout: "vertical", gap: ts(2) } satisfies Layout;