mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-03 19:12:50 +01:00
fix(favorites): sync empty-state on cache hits to avoid blank view
Switching Favorites<->Watchlist (props swap in place, no remount) left the page blank with only the tab buttons: the empty-state was written as a side effect inside the queryFn, which React Query skips on cache hits, and a reset effect then cleared it — so on a cache-served switch nothing repopulated it, every list was hidden by hideIfEmpty, and the empty message never rendered. Each list now reports emptiness once its query settles (incl. cache hits) via a new optional onEmptyStateChange callback on InfiniteScrollingCollectionList (mobile + TV), reporting null while loading so a switch never flashes a stale state. The parent derives the aggregate empty-state from that (null = not settled yet, so the message stays hidden during a switch). The reset effect is removed entirely, not kept: React runs child effects before parent effects within a commit, so a parent reset setEmptyState(false) would clobber the children's reported values on the same render. Dropping it and using the per-list callbacks (tri-state null/true/false) is what makes the switch — including a cache-served one — resolve to the correct state. Applies to both mobile and TV Favorites.
This commit is contained in:
@@ -7,7 +7,7 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
|||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Text, View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
// PNG ASSET
|
// PNG ASSET
|
||||||
import heart from "@/assets/icons/heart.fill.png";
|
import heart from "@/assets/icons/heart.fill.png";
|
||||||
@@ -23,7 +23,9 @@ type FavoriteTypes =
|
|||||||
| "Video"
|
| "Video"
|
||||||
| "BoxSet"
|
| "BoxSet"
|
||||||
| "Playlist";
|
| "Playlist";
|
||||||
type EmptyState = Record<FavoriteTypes, boolean>;
|
// `null` = not settled yet (loading/unknown); avoids flashing the empty
|
||||||
|
// message during a favorites/watchlist switch before the new queries resolve.
|
||||||
|
type EmptyState = Record<FavoriteTypes, boolean | null>;
|
||||||
|
|
||||||
interface FavoritesProps {
|
interface FavoritesProps {
|
||||||
/** Jellyfin item filter. "IsFavorite" (default) or "Likes" for the watchlist view. */
|
/** Jellyfin item filter. "IsFavorite" (default) or "Likes" for the watchlist view. */
|
||||||
@@ -48,12 +50,12 @@ export const Favorites = ({
|
|||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
const pageSize = 20;
|
const pageSize = 20;
|
||||||
const [emptyState, setEmptyState] = useState<EmptyState>({
|
const [emptyState, setEmptyState] = useState<EmptyState>({
|
||||||
Series: false,
|
Series: null,
|
||||||
Movie: false,
|
Movie: null,
|
||||||
Episode: false,
|
Episode: null,
|
||||||
Video: false,
|
Video: null,
|
||||||
BoxSet: false,
|
BoxSet: null,
|
||||||
Playlist: false,
|
Playlist: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchFavoritesByType = useCallback(
|
const fetchFavoritesByType = useCallback(
|
||||||
@@ -76,42 +78,28 @@ export const Favorites = ({
|
|||||||
limit: limit,
|
limit: limit,
|
||||||
includeItemTypes: [itemType],
|
includeItemTypes: [itemType],
|
||||||
});
|
});
|
||||||
const items = response.data.Items || [];
|
return response.data.Items || [];
|
||||||
|
|
||||||
// Update empty state for this specific type only for the first page
|
|
||||||
if (startIndex === 0) {
|
|
||||||
setEmptyState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[itemType as FavoriteTypes]: items.length === 0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
},
|
},
|
||||||
[api, user, filter],
|
[api, user, filter],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset empty state when the account or active view changes. `filter`
|
// Emptiness is reported by each list once its query settles (incl. cache
|
||||||
// matters because switching the favorites/watchlist toggle swaps this
|
// hits), so it stays correct where a queryFn side effect would go stale.
|
||||||
// component's props in place (no remount), so stale per-type emptiness
|
const setTypeEmpty = useCallback(
|
||||||
// from the previous view must be cleared until the new queries resolve.
|
(type: FavoriteTypes, isEmpty: boolean | null) =>
|
||||||
useEffect(() => {
|
setEmptyState((prev) =>
|
||||||
setEmptyState({
|
prev[type] === isEmpty ? prev : { ...prev, [type]: isEmpty },
|
||||||
Series: false,
|
),
|
||||||
Movie: false,
|
[],
|
||||||
Episode: false,
|
);
|
||||||
Video: false,
|
|
||||||
BoxSet: false,
|
|
||||||
Playlist: false,
|
|
||||||
});
|
|
||||||
}, [api, user, filter]);
|
|
||||||
|
|
||||||
// Check if all categories that have been loaded are empty
|
// Show the empty message only once every category has settled AND is empty.
|
||||||
|
// A `null` (still loading) keeps it hidden, so switching favorites/watchlist
|
||||||
|
// (props swap in place, no remount) never flashes a stale empty state.
|
||||||
const areAllEmpty = () => {
|
const areAllEmpty = () => {
|
||||||
const loadedCategories = Object.values(emptyState);
|
const categories = Object.values(emptyState);
|
||||||
return (
|
return (
|
||||||
loadedCategories.length > 0 &&
|
categories.length > 0 && categories.every((isEmpty) => isEmpty === true)
|
||||||
loadedCategories.every((isEmpty) => isEmpty)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,6 +179,7 @@ export const Favorites = ({
|
|||||||
title={t("favorites.series")}
|
title={t("favorites.series")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Series", isEmpty)}
|
||||||
onPressSeeAll={() => seeAll("Series", "Series")}
|
onPressSeeAll={() => seeAll("Series", "Series")}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
@@ -200,6 +189,7 @@ export const Favorites = ({
|
|||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
orientation='vertical'
|
orientation='vertical'
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Movie", isEmpty)}
|
||||||
onPressSeeAll={() => seeAll("Movie", "Movies")}
|
onPressSeeAll={() => seeAll("Movie", "Movies")}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
@@ -208,6 +198,7 @@ export const Favorites = ({
|
|||||||
title={t("favorites.episodes")}
|
title={t("favorites.episodes")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Episode", isEmpty)}
|
||||||
onPressSeeAll={() => seeAll("Episode", "Episodes")}
|
onPressSeeAll={() => seeAll("Episode", "Episodes")}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
@@ -216,6 +207,7 @@ export const Favorites = ({
|
|||||||
title={t("favorites.videos")}
|
title={t("favorites.videos")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Video", isEmpty)}
|
||||||
onPressSeeAll={() => seeAll("Video", "Videos")}
|
onPressSeeAll={() => seeAll("Video", "Videos")}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
@@ -224,6 +216,7 @@ export const Favorites = ({
|
|||||||
title={t("favorites.boxsets")}
|
title={t("favorites.boxsets")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("BoxSet", isEmpty)}
|
||||||
onPressSeeAll={() => seeAll("BoxSet", "Boxsets")}
|
onPressSeeAll={() => seeAll("BoxSet", "Boxsets")}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
@@ -232,6 +225,7 @@ export const Favorites = ({
|
|||||||
title={t("favorites.playlists")}
|
title={t("favorites.playlists")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Playlist", isEmpty)}
|
||||||
onPressSeeAll={() => seeAll("Playlist", "Playlists")}
|
onPressSeeAll={() => seeAll("Playlist", "Playlists")}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type {
|
|||||||
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ScrollView, View } from "react-native";
|
import { ScrollView, View } from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
@@ -30,7 +30,9 @@ type FavoriteTypes =
|
|||||||
| "Video"
|
| "Video"
|
||||||
| "BoxSet"
|
| "BoxSet"
|
||||||
| "Playlist";
|
| "Playlist";
|
||||||
type EmptyState = Record<FavoriteTypes, boolean>;
|
// `null` = not settled yet (loading/unknown); avoids flashing the empty
|
||||||
|
// message during a favorites/watchlist switch before the new queries resolve.
|
||||||
|
type EmptyState = Record<FavoriteTypes, boolean | null>;
|
||||||
|
|
||||||
export const Favorites = () => {
|
export const Favorites = () => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
@@ -60,12 +62,12 @@ export const Favorites = () => {
|
|||||||
const emptyTextKey = `${emptyNamespace}.noData`;
|
const emptyTextKey = `${emptyNamespace}.noData`;
|
||||||
|
|
||||||
const [emptyState, setEmptyState] = useState<EmptyState>({
|
const [emptyState, setEmptyState] = useState<EmptyState>({
|
||||||
Series: false,
|
Series: null,
|
||||||
Movie: false,
|
Movie: null,
|
||||||
Episode: false,
|
Episode: null,
|
||||||
Video: false,
|
Video: null,
|
||||||
BoxSet: false,
|
BoxSet: null,
|
||||||
Playlist: false,
|
Playlist: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchFavoritesByType = useCallback(
|
const fetchFavoritesByType = useCallback(
|
||||||
@@ -88,36 +90,28 @@ export const Favorites = () => {
|
|||||||
limit: limit,
|
limit: limit,
|
||||||
includeItemTypes: [itemType],
|
includeItemTypes: [itemType],
|
||||||
});
|
});
|
||||||
const items = response.data.Items || [];
|
return response.data.Items || [];
|
||||||
|
|
||||||
if (startIndex === 0) {
|
|
||||||
setEmptyState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[itemType as FavoriteTypes]: items.length === 0,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
},
|
},
|
||||||
[api, user, filter],
|
[api, user, filter],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
// Emptiness is reported by each list once its query settles (incl. cache
|
||||||
setEmptyState({
|
// hits), so it stays correct where a queryFn side effect would go stale.
|
||||||
Series: false,
|
const setTypeEmpty = useCallback(
|
||||||
Movie: false,
|
(type: FavoriteTypes, isEmpty: boolean | null) =>
|
||||||
Episode: false,
|
setEmptyState((prev) =>
|
||||||
Video: false,
|
prev[type] === isEmpty ? prev : { ...prev, [type]: isEmpty },
|
||||||
BoxSet: false,
|
),
|
||||||
Playlist: false,
|
[],
|
||||||
});
|
);
|
||||||
}, [api, user, viewType]);
|
|
||||||
|
|
||||||
|
// Show the empty message only once every category has settled AND is empty.
|
||||||
|
// A `null` (still loading) keeps it hidden, so switching favorites/watchlist
|
||||||
|
// never flashes a stale empty state.
|
||||||
const areAllEmpty = () => {
|
const areAllEmpty = () => {
|
||||||
const loadedCategories = Object.values(emptyState);
|
const categories = Object.values(emptyState);
|
||||||
return (
|
return (
|
||||||
loadedCategories.length > 0 &&
|
categories.length > 0 && categories.every((isEmpty) => isEmpty === true)
|
||||||
loadedCategories.every((isEmpty) => isEmpty)
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,6 +229,7 @@ export const Favorites = () => {
|
|||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
isFirstSection={!watchlistEnabled}
|
isFirstSection={!watchlistEnabled}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Series", isEmpty)}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
queryFn={fetchFavoriteMovies}
|
queryFn={fetchFavoriteMovies}
|
||||||
@@ -243,6 +238,7 @@ export const Favorites = () => {
|
|||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
orientation='vertical'
|
orientation='vertical'
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Movie", isEmpty)}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
queryFn={fetchFavoriteEpisodes}
|
queryFn={fetchFavoriteEpisodes}
|
||||||
@@ -250,6 +246,7 @@ export const Favorites = () => {
|
|||||||
title={t("favorites.episodes")}
|
title={t("favorites.episodes")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Episode", isEmpty)}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
queryFn={fetchFavoriteVideos}
|
queryFn={fetchFavoriteVideos}
|
||||||
@@ -257,6 +254,7 @@ export const Favorites = () => {
|
|||||||
title={t("favorites.videos")}
|
title={t("favorites.videos")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Video", isEmpty)}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
queryFn={fetchFavoriteBoxsets}
|
queryFn={fetchFavoriteBoxsets}
|
||||||
@@ -264,6 +262,7 @@ export const Favorites = () => {
|
|||||||
title={t("favorites.boxsets")}
|
title={t("favorites.boxsets")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("BoxSet", isEmpty)}
|
||||||
/>
|
/>
|
||||||
<InfiniteScrollingCollectionList
|
<InfiniteScrollingCollectionList
|
||||||
queryFn={fetchFavoritePlaylists}
|
queryFn={fetchFavoritePlaylists}
|
||||||
@@ -271,6 +270,7 @@ export const Favorites = () => {
|
|||||||
title={t("favorites.playlists")}
|
title={t("favorites.playlists")}
|
||||||
hideIfEmpty
|
hideIfEmpty
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
|
onEmptyStateChange={(isEmpty) => setTypeEmpty("Playlist", isEmpty)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ interface Props extends ViewProps {
|
|||||||
onPressSeeAll?: () => void;
|
onPressSeeAll?: () => void;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
onLoaded?: () => void;
|
onLoaded?: () => void;
|
||||||
|
/**
|
||||||
|
* Reports emptiness whenever the query settles (incl. cache hits):
|
||||||
|
* `null` while loading (unknown), otherwise whether the list is empty.
|
||||||
|
* Lets a parent derive an aggregate empty-state reactively instead of via a
|
||||||
|
* queryFn side effect, which React Query skips when it serves cache.
|
||||||
|
*/
|
||||||
|
onEmptyStateChange?: (isEmpty: boolean | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||||
@@ -45,6 +52,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
onPressSeeAll,
|
onPressSeeAll,
|
||||||
enabled = true,
|
enabled = true,
|
||||||
onLoaded,
|
onLoaded,
|
||||||
|
onEmptyStateChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const effectivePageSize = Math.max(1, pageSize);
|
const effectivePageSize = Math.max(1, pageSize);
|
||||||
@@ -103,6 +111,14 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
return deduped;
|
return deduped;
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// Report emptiness on every settle (incl. cache hits). Callback held in a ref
|
||||||
|
// so an inline parent callback doesn't retrigger the effect each render.
|
||||||
|
const onEmptyStateChangeRef = useRef(onEmptyStateChange);
|
||||||
|
onEmptyStateChangeRef.current = onEmptyStateChange;
|
||||||
|
useEffect(() => {
|
||||||
|
onEmptyStateChangeRef.current?.(isLoading ? null : allItems.length === 0);
|
||||||
|
}, [isLoading, allItems.length]);
|
||||||
|
|
||||||
const snapOffsets = useMemo(() => {
|
const snapOffsets = useMemo(() => {
|
||||||
const itemWidth = orientation === "horizontal" ? 184 : 120; // w-44 (176px) + mr-2 (8px) or w-28 (112px) + mr-2 (8px)
|
const itemWidth = orientation === "horizontal" ? 184 : 120; // w-44 (176px) + mr-2 (8px) or w-28 (112px) + mr-2 (8px)
|
||||||
return allItems.map((_, index) => index * itemWidth);
|
return allItems.map((_, index) => index * itemWidth);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import { useSegments } from "expo-router";
|
import { useSegments } from "expo-router";
|
||||||
import { useCallback, useMemo, useRef } from "react";
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -42,6 +42,13 @@ interface Props extends ViewProps {
|
|||||||
isFirstSection?: boolean;
|
isFirstSection?: boolean;
|
||||||
onItemFocus?: (item: BaseItemDto) => void;
|
onItemFocus?: (item: BaseItemDto) => void;
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
|
/**
|
||||||
|
* Reports emptiness whenever the query settles (incl. cache hits):
|
||||||
|
* `null` while loading (unknown), otherwise whether the list is empty.
|
||||||
|
* Lets a parent derive an aggregate empty-state reactively instead of via a
|
||||||
|
* queryFn side effect, which React Query skips when it serves cache.
|
||||||
|
*/
|
||||||
|
onEmptyStateChange?: (isEmpty: boolean | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Typography = ReturnType<typeof useScaledTVTypography>;
|
type Typography = ReturnType<typeof useScaledTVTypography>;
|
||||||
@@ -123,6 +130,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
isFirstSection = false,
|
isFirstSection = false,
|
||||||
onItemFocus,
|
onItemFocus,
|
||||||
parentId,
|
parentId,
|
||||||
|
onEmptyStateChange,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const typography = useScaledTVTypography();
|
const typography = useScaledTVTypography();
|
||||||
@@ -182,6 +190,14 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
return deduped;
|
return deduped;
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// Report emptiness on every settle (incl. cache hits). Callback held in a ref
|
||||||
|
// so an inline parent callback doesn't retrigger the effect each render.
|
||||||
|
const onEmptyStateChangeRef = useRef(onEmptyStateChange);
|
||||||
|
onEmptyStateChangeRef.current = onEmptyStateChange;
|
||||||
|
useEffect(() => {
|
||||||
|
onEmptyStateChangeRef.current?.(isLoading ? null : allItems.length === 0);
|
||||||
|
}, [isLoading, allItems.length]);
|
||||||
|
|
||||||
const itemWidth =
|
const itemWidth =
|
||||||
orientation === "horizontal" ? posterSizes.episode : posterSizes.poster;
|
orientation === "horizontal" ? posterSizes.episode : posterSizes.poster;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user