From 820604180de566910d64c7673f6ef4d8f3e328de Mon Sep 17 00:00:00 2001 From: Gauvain Date: Wed, 15 Jul 2026 19:01:32 +0200 Subject: [PATCH] feat(appearance): use episode images for Next Up & Continue Watching (opt-in) --- app/(auth)/(tabs)/(home)/settings.tv.tsx | 7 +++++++ .../home/InfiniteScrollingCollectionList.tsx | 7 ++++++- .../InfiniteScrollingCollectionList.tv.tsx | 4 ++++ components/home/ScrollingCollectionList.tsx | 7 ++++++- components/settings/AppearanceSettings.tsx | 21 +++++++++++++++++++ components/tv/TVPosterCard.tsx | 10 ++++++++- translations/en.json | 5 +++++ utils/atoms/settings.ts | 4 ++++ 8 files changed, 62 insertions(+), 3 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/settings.tv.tsx b/app/(auth)/(tabs)/(home)/settings.tv.tsx index a9a2e2fbb..5f94fad70 100644 --- a/app/(auth)/(tabs)/(home)/settings.tv.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tv.tsx @@ -854,6 +854,13 @@ export default function SettingsTV() { updateSettings({ mergeNextUpAndContinueWatching: value }) } /> + + updateSettings({ useEpisodeImagesForNextUp: value }) + } + /> = ({ }, [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 = ({ `} > {item.Type === "Episode" && orientation === "horizontal" && ( - + )} {item.Type === "Episode" && orientation === "vertical" && ( diff --git a/components/home/InfiniteScrollingCollectionList.tv.tsx b/components/home/InfiniteScrollingCollectionList.tv.tsx index 1db314f2b..585135f6b 100644 --- a/components/home/InfiniteScrollingCollectionList.tv.tsx +++ b/components/home/InfiniteScrollingCollectionList.tv.tsx @@ -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 = ({ }); const { t } = useTranslation(); + const { settings } = useSettings(); const allItems = useMemo(() => { const items = data?.pages.flat() ?? []; @@ -231,6 +233,7 @@ export const InfiniteScrollingCollectionList: React.FC = ({ hasTVPreferredFocus={isFirstItem} onFocus={() => handleItemFocus(item)} width={itemWidth} + preferEpisodeImage={settings?.useEpisodeImagesForNextUp} /> ); @@ -243,6 +246,7 @@ export const InfiniteScrollingCollectionList: React.FC = ({ showItemActions, handleItemFocus, ITEM_GAP, + settings?.useEpisodeImagesForNextUp, ], ); diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index 185ad8c2f..a09ce6f0e 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -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 = ({ }); 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 = ({ `} > {item.Type === "Episode" && orientation === "horizontal" && ( - + )} {item.Type === "Episode" && orientation === "vertical" && ( diff --git a/components/settings/AppearanceSettings.tsx b/components/settings/AppearanceSettings.tsx index 844096177..8ce41dedb 100644 --- a/components/settings/AppearanceSettings.tsx +++ b/components/settings/AppearanceSettings.tsx @@ -27,6 +27,7 @@ export const AppearanceSettings: React.FC = () => { Linking.openURL( @@ -44,6 +45,9 @@ export const AppearanceSettings: React.FC = () => { { } /> + + + updateSettings({ useEpisodeImagesForNextUp: value }) + } + /> + router.push("/settings/appearance/hide-libraries/page") } title={t("home.settings.other.hide_libraries")} + subtitle={t("home.settings.other.select_libraries_you_want_to_hide")} showArrow /> string | undefined; + + /** For horizontal episodes, prefer the episode's own image over the series thumb */ + preferEpisodeImage?: boolean; } /** @@ -108,6 +111,7 @@ export const TVPosterCard: React.FC = ({ glowColor = "white", scaleAmount = 1.05, imageUrlGetter, + preferEpisodeImage = false, }) => { const api = useAtomValue(apiAtom); const posterSizes = useScaledTVPosterSizes(); @@ -139,6 +143,10 @@ export const TVPosterCard: React.FC = ({ if (orientation === "horizontal") { // Episode: prefer series thumb image for consistent look (like hero section) if (item.Type === "Episode") { + // Opt-in: use the episode's own image instead of the series thumb. + if (preferEpisodeImage && item.ImageTags?.Primary) { + return `${api.basePath}/Items/${item.Id}/Images/Primary?fillHeight=600&quality=80&tag=${item.ImageTags.Primary}`; + } // First try parent/series thumb (horizontal series artwork) if (item.ParentBackdropItemId && item.ParentThumbImageTag) { return `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=700&quality=80&tag=${item.ParentThumbImageTag}`; @@ -173,7 +181,7 @@ export const TVPosterCard: React.FC = ({ item, width: width * 2, // 2x for quality on large screens }); - }, [api, item, orientation, width, imageUrlGetter]); + }, [api, item, orientation, width, imageUrlGetter, preferEpisodeImage]); // Progress calculation const progress = useMemo(() => { diff --git a/translations/en.json b/translations/en.json index 692532eb7..4a0b3f647 100644 --- a/translations/en.json +++ b/translations/en.json @@ -137,7 +137,11 @@ "appearance": { "title": "Appearance", "merge_next_up_continue_watching": "Merge Continue watching & Next up", + "merge_next_up_continue_watching_hint": "Combine Continue Watching and Next Up into a single home row.", + "use_episode_images_next_up": "Use episode images for Next Up & Continue Watching", + "use_episode_images_next_up_hint": "Show each episode's own thumbnail in the Next Up and Continue Watching rows instead of the series image.", "hide_remote_session_button": "Hide remote session button", + "hide_remote_session_button_hint": "Hide the remote-sessions button from the home header.", "show_home_backdrop": "Dynamic home backdrop", "show_hero_carousel": "Hero carousel", "show_series_poster_on_episode": "Show series poster on episodes", @@ -296,6 +300,7 @@ }, "safe_area_in_controls": "Safe area in controls", "show_custom_menu_links": "Show custom menu links", + "show_custom_menu_links_hint": "Show the custom links your server administrator added in the web config.", "show_large_home_carousel": "Show large home carousel (beta)", "hide_libraries": "Hide libraries", "select_libraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.", diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 358550be4..7b7452659 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -274,6 +274,9 @@ export type Settings = { hideBrightnessSlider: boolean; usePopularPlugin: boolean; mergeNextUpAndContinueWatching: boolean; + // Use the episode's own image (instead of the series thumb) for the + // "Next Up" and "Continue Watching" home rows. + useEpisodeImagesForNextUp: boolean; // TV-specific settings showHomeBackdrop: boolean; showTVHeroCarousel: boolean; @@ -382,6 +385,7 @@ export const defaultValues: Settings = { hideBrightnessSlider: false, usePopularPlugin: true, mergeNextUpAndContinueWatching: false, + useEpisodeImagesForNextUp: false, // TV-specific settings showHomeBackdrop: true, showTVHeroCarousel: true,