feat(appearance): episode images for Next Up & Continue Watching

- Fix black episode thumbnails in the Next Up / Continue Watching rows:
  build the parent Thumb URL from the matched ParentThumbItemId +
  ParentThumbImageTag pair, instead of pairing ParentBackdropItemId with
  the thumb tag (different parent -> 404 -> black). Fixed on mobile
  (ContinueWatchingPoster) and TV (TVPosterCard, TVHeroCarousel).
- Add a "Use episode images for Next Up & Continue Watching" setting
  (default off = series image, matching Jellyfin), wired into the home
  rows on mobile and TV.
- Add helper descriptions under the Appearance settings rows.
This commit is contained in:
Gauvain
2026-06-28 01:22:45 +02:00
parent 4a0dc44251
commit 8a781f2462
10 changed files with 77 additions and 12 deletions

View File

@@ -15,6 +15,7 @@ import {
import { SectionHeader } from "@/components/common/SectionHeader";
import { Text } from "@/components/common/Text";
import MoviePoster from "@/components/posters/MoviePoster";
import { useSettings } from "@/utils/atoms/settings";
import { Colors } from "../../constants/Colors";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
@@ -85,6 +86,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
}, [isSuccess, onLoaded]);
const { t } = useTranslation();
const { settings } = useSettings();
// Flatten all pages into a single array (and de-dupe by Id to avoid UI duplicates)
const allItems = useMemo(() => {
@@ -186,7 +188,10 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
`}
>
{item.Type === "Episode" && orientation === "horizontal" && (
<ContinueWatchingPoster item={item} />
<ContinueWatchingPoster
item={item}
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
/>
)}
{item.Type === "Episode" && orientation === "vertical" && (
<SeriesPoster item={item} />

View File

@@ -24,6 +24,7 @@ import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter";
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
import { useSettings } from "@/utils/atoms/settings";
import { scaleSize } from "@/utils/scaleSize";
// Extra padding to accommodate scale animation (1.05x) and glow shadow
@@ -165,6 +166,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
});
const { t } = useTranslation();
const { settings } = useSettings();
const allItems = useMemo(() => {
const items = data?.pages.flat() ?? [];
@@ -225,6 +227,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
hasTVPreferredFocus={isFirstItem}
onFocus={() => handleItemFocus(item)}
width={itemWidth}
preferEpisodeImage={settings?.useEpisodeImagesForNextUp}
/>
</View>
);
@@ -237,6 +240,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
showItemActions,
handleItemFocus,
ITEM_GAP,
settings?.useEpisodeImagesForNextUp,
],
);

View File

@@ -9,6 +9,7 @@ import { ScrollView, View, type ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import MoviePoster from "@/components/posters/MoviePoster";
import { useInView } from "@/hooks/useInView";
import { useSettings } from "@/utils/atoms/settings";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { TouchableItemRouter } from "../common/TouchableItemRouter";
import { ItemCardText } from "../ItemCardText";
@@ -50,6 +51,7 @@ export const ScrollingCollectionList: React.FC<Props> = ({
});
const { t } = useTranslation();
const { settings } = useSettings();
// Show skeleton if loading OR if lazy loading is enabled and not in view yet
const shouldShowSkeleton = isLoading || (enableLazyLoading && !isInView);
@@ -108,7 +110,10 @@ export const ScrollingCollectionList: React.FC<Props> = ({
`}
>
{item.Type === "Episode" && orientation === "horizontal" && (
<ContinueWatchingPoster item={item} />
<ContinueWatchingPoster
item={item}
useEpisodePoster={settings?.useEpisodeImagesForNextUp}
/>
)}
{item.Type === "Episode" && orientation === "vertical" && (
<SeriesPoster item={item} />

View File

@@ -65,10 +65,11 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
const posterUrl = useMemo(() => {
if (!api) return null;
// For episodes, always use series thumb
// For episodes, always use series thumb.
// Matched pair: ParentThumbItemId owns the Thumb tag, not ParentBackdropItemId.
if (item.Type === "Episode") {
if (item.ParentThumbImageTag) {
return `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=400&quality=80&tag=${item.ParentThumbImageTag}`;
if (item.ParentThumbItemId && item.ParentThumbImageTag) {
return `${api.basePath}/Items/${item.ParentThumbItemId}/Images/Thumb?fillHeight=400&quality=80&tag=${item.ParentThumbImageTag}`;
}
if (item.SeriesId) {
return `${api.basePath}/Items/${item.SeriesId}/Images/Thumb?fillHeight=400&quality=80`;