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) {
return airDate.getFullYear().toString();
}
return null;
};
export const useLocalSetting = (setting: string, def: string) => {

View File

@ -93,3 +93,10 @@ export const Image = ({
</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>
);
};
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"] }>;
}) => <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 = ({
alt,
layout,

View File

@ -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 })}
>
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
{type === "movie" && watchPercent && <ItemProgress watchPercent={watchPercent} />}
{slug && watchStatus !== undefined && type && type !== "collection" && (
{type !== "collection" && (
<ItemContext
type={type}
slug={slug}
@ -198,34 +196,51 @@ export const ItemGrid = ({
/>
)}
</PosterBackground>
<Skeleton>
{isLoading || (
<P
numberOfLines={subtitle ? 1 : 2}
{...css([{ marginY: 0, textAlign: "center" }, "title"])}
>
{name}
</P>
)}
</Skeleton>
{(isLoading || subtitle) && (
<Skeleton {...css({ width: percent(50) })}>
{isLoading || (
<SubP
{...css({
marginTop: 0,
textAlign: "center",
})}
>
{subtitle}
</SubP>
)}
</Skeleton>
<P numberOfLines={subtitle ? 1 : 2} {...css([{ marginY: 0, textAlign: "center" }, "title"])}>
{name}
</P>
{subtitle && (
<SubP
{...css({
marginTop: 0,
textAlign: "center",
})}
>
{subtitle}
</SubP>
)}
</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 = {
size: px(150),
numColumns: { xs: 3, sm: 4, md: 5, lg: 6, xl: 8 },

View File

@ -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<LibraryItem>,
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
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<typeof ItemGrid> & ComponentProps<typeof ItemList> => ({
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<LibraryItem> => ({
parser: LibraryItemP,
@ -92,9 +86,9 @@ export const BrowsePage: QueryPage = () => {
setLayout={setLayout}
/>
}
>
{(item) => <LayoutComponent {...itemMap(item)} />}
</InfiniteFetch>
Render={({ item }) => <LayoutComponent {...itemMap(item)} />}
Loader={() => <LayoutComponent.Loader />}
/>
);
};

View File

@ -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",
})}
>
<Skeleton {...css({ height: rem(2), alignSelf: "center" })}>
{isLoading || (
<Heading
{...css([
"title",
{
textAlign: "center",
fontSize: rem(2),
letterSpacing: rem(0.002),
fontWeight: "900",
textTransform: "uppercase",
},
])}
>
{name}
</Heading>
)}
</Skeleton>
{slug && watchStatus !== undefined && type && type !== "collection" && (
<Heading
{...css([
"title",
{
textAlign: "center",
fontSize: rem(2),
letterSpacing: rem(0.002),
fontWeight: "900",
textTransform: "uppercase",
},
])}
>
{name}
</Heading>
{type !== "collection" && (
<ItemContext
type={type}
slug={slug}
@ -151,32 +148,56 @@ export const ItemList = ({
/>
)}
</View>
{(isLoading || subtitle) && (
<Skeleton {...css({ width: rem(5), alignSelf: "center" })}>
{isLoading || (
<P
{...css({
textAlign: "center",
marginRight: ts(4),
})}
>
{subtitle}
</P>
)}
</Skeleton>
{subtitle && (
<P
{...css({
textAlign: "center",
marginRight: ts(4),
})}
>
{subtitle}
</P>
)}
</View>
<PosterBackground
src={poster}
alt=""
quality="low"
forcedLoading={isLoading}
layout={{ height: percent(80) }}
>
<PosterBackground src={poster} alt="" quality="low" layout={{ height: percent(80) }}>
<ItemWatchStatus watchStatus={watchStatus} unseenEpisodesCount={unseenEpisodesCount} />
</PosterBackground>
</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;