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"] }>;
}) => ;
+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 })}
+