mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Split loaders for most items on the main page
This commit is contained in:
parent
2756397898
commit
393c58b10a
@ -213,7 +213,7 @@ export const ItemGrid = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ItemGrid.Loader = (props: Stylable) => {
|
ItemGrid.Loader = (props: object) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -87,7 +87,7 @@ export const BrowsePage: QueryPage = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
Render={({ item }) => <LayoutComponent {...itemMap(item)} />}
|
Render={({ item }) => <LayoutComponent {...itemMap(item)} />}
|
||||||
Loader={() => <LayoutComponent.Loader />}
|
Loader={LayoutComponent.Loader}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 (
|
||||||
|
@ -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,
|
||||||
|
@ -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";
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 />)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user