From c12b58e5cb3bc42435d0edc754c9c40c26759ea9 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 26 Aug 2024 08:30:12 +0200 Subject: [PATCH] fix: posters --- app/(auth)/(tabs)/(home)/index.tsx | 2 +- .../(home,libraries,search)/items/[id].tsx | 49 ++++++++++++++----- components/ContinueWatchingPoster.tsx | 5 ++ components/ParallaxPage.tsx | 32 +++++++++--- components/series/NextUp.tsx | 2 +- components/series/SeasonPicker.tsx | 6 ++- utils/jellyfin/image/getBackdropUrl.ts | 4 ++ utils/jellyfin/image/getLogoImageUrlById.ts | 24 +++++++-- .../image/getParentBackdropImageUrl.ts | 42 ++++++++++++++++ .../image/getPrimaryParentImageUrl.ts | 42 ++++++++++++++++ 10 files changed, 179 insertions(+), 29 deletions(-) create mode 100644 utils/jellyfin/image/getParentBackdropImageUrl.ts create mode 100644 utils/jellyfin/image/getPrimaryParentImageUrl.ts diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index ac25bacb..fc78cc50 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -222,7 +222,7 @@ export default function index() { }) ).data.Items || [], type: "ScrollingCollectionList", - orientation: "vertical", + orientation: "horizontal", }, ]; return ss; diff --git a/app/(auth)/(tabs)/(home,libraries,search)/items/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search)/items/[id].tsx index e62f619a..0a517ff0 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/items/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/items/[id].tsx @@ -19,6 +19,8 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; +import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl"; +import { getPrimaryParentImageUrl } from "@/utils/jellyfin/image/getPrimaryParentImageUrl"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; import { chromecastProfile } from "@/utils/profiles/chromecast"; @@ -125,7 +127,7 @@ const page: React.FC = () => { staleTime: 0, }); - const backdropUrl = useMemo( + const itemBackdropUrl = useMemo( () => getBackdropUrl({ api, @@ -136,8 +138,24 @@ const page: React.FC = () => { [item] ); - const logoUrl = useMemo( - () => (item?.Type === "Movie" ? getLogoImageUrlById({ api, item }) : null), + const seriesBackdropUrl = useMemo( + () => + getParentBackdropImageUrl({ + api, + item, + quality: 95, + width: 1200, + }), + [item] + ); + + const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]); + + const episodePoster = useMemo( + () => + item?.Type === "Episode" + ? `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80` + : null, [item] ); @@ -148,20 +166,25 @@ const page: React.FC = () => { ); - if (!item?.Id || !backdropUrl) return null; + if (!item?.Id) return null; return ( + <> + {itemBackdropUrl ? ( + + ) : null} + } logo={ <> diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index 31b3f5ea..c93ec275 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -9,11 +9,13 @@ import { WatchedIndicator } from "./WatchedIndicator"; type ContinueWatchingPosterProps = { item: BaseItemDto; width?: number; + useEpisodePoster?: boolean; }; const ContinueWatchingPoster: React.FC = ({ item, width = 176, + useEpisodePoster = false, }) => { const [api] = useAtom(apiAtom); @@ -22,6 +24,9 @@ const ContinueWatchingPoster: React.FC = ({ */ const url = useMemo(() => { if (!api) return; + if (item.Type === "Episode" && useEpisodePoster) { + return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`; + } if (item.Type === "Episode") { if (item.ParentBackdropItemId && item.ParentThumbImageTag) return `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`; diff --git a/components/ParallaxPage.tsx b/components/ParallaxPage.tsx index e970f1a3..595de780 100644 --- a/components/ParallaxPage.tsx +++ b/components/ParallaxPage.tsx @@ -1,4 +1,4 @@ -import type { PropsWithChildren, ReactElement } from "react"; +import { useMemo, type PropsWithChildren, type ReactElement } from "react"; import { View } from "react-native"; import Animated, { interpolate, @@ -8,16 +8,18 @@ import Animated, { } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -const HEADER_HEIGHT = 400; - type Props = PropsWithChildren<{ headerImage: ReactElement; logo?: ReactElement; + episodePoster?: ReactElement; + headerHeight?: number; }>; export const ParallaxScrollView: React.FC = ({ children, headerImage, + episodePoster, + headerHeight = 400, logo, }: Props) => { const scrollRef = useAnimatedRef(); @@ -29,14 +31,14 @@ export const ParallaxScrollView: React.FC = ({ { translateY: interpolate( scrollOffset.value, - [-HEADER_HEIGHT, 0, HEADER_HEIGHT], - [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75] + [-headerHeight, 0, headerHeight], + [-headerHeight / 2, 0, headerHeight * 0.75] ), }, { scale: interpolate( scrollOffset.value, - [-HEADER_HEIGHT, 0, HEADER_HEIGHT], + [-headerHeight, 0, headerHeight], [2, 1, 1] ), }, @@ -56,15 +58,29 @@ export const ParallaxScrollView: React.FC = ({ scrollEventThrottle={16} > {logo && ( - + {logo} )} + {episodePoster && ( + + + {episodePoster} + + + )} + = ({ seriesId }) => { key={item.Id} className="flex flex-col w-44" > - + )} diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index 7f85e0f3..ef6ce3de 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -188,7 +188,11 @@ export const SeasonPicker: React.FC = ({ item }) => { > - + diff --git a/utils/jellyfin/image/getBackdropUrl.ts b/utils/jellyfin/image/getBackdropUrl.ts index 7fdf7efe..dd138aab 100644 --- a/utils/jellyfin/image/getBackdropUrl.ts +++ b/utils/jellyfin/image/getBackdropUrl.ts @@ -36,6 +36,10 @@ export const getBackdropUrl = ({ params.append("fillWidth", width.toString()); } + if (item.Type === "Episode") { + return getPrimaryImageUrl({ api, item, quality, width }); + } + if (backdropImageTags) { params.append("tag", backdropImageTags); return `${api.basePath}/Items/${ diff --git a/utils/jellyfin/image/getLogoImageUrlById.ts b/utils/jellyfin/image/getLogoImageUrlById.ts index a801b1b7..3712b888 100644 --- a/utils/jellyfin/image/getLogoImageUrlById.ts +++ b/utils/jellyfin/image/getLogoImageUrlById.ts @@ -21,15 +21,29 @@ export const getLogoImageUrlById = ({ return null; } - const imageTags = item.ImageTags?.["Logo"]; - - if (!imageTags) return null; - const params = new URLSearchParams(); - params.append("tag", imageTags); params.append("quality", "90"); params.append("fillHeight", height.toString()); + if (item.Type === "Episode") { + const imageTag = item.ParentLogoImageTag; + const parentId = item.ParentLogoItemId; + + if (!parentId || !imageTag) { + return null; + } + + params.append("tag", imageTag); + + return `${api.basePath}/Items/${parentId}/Images/Logo?${params.toString()}`; + } + + const imageTag = item.ImageTags?.["Logo"]; + + if (!imageTag) return null; + + params.append("tag", imageTag); + return `${api.basePath}/Items/${item.Id}/Images/Logo?${params.toString()}`; }; diff --git a/utils/jellyfin/image/getParentBackdropImageUrl.ts b/utils/jellyfin/image/getParentBackdropImageUrl.ts new file mode 100644 index 00000000..4a03795b --- /dev/null +++ b/utils/jellyfin/image/getParentBackdropImageUrl.ts @@ -0,0 +1,42 @@ +import { Api } from "@jellyfin/sdk"; +import { + BaseItemDto, + BaseItemPerson, +} from "@jellyfin/sdk/lib/generated-client/models"; +import { isBaseItemDto } from "../jellyfin"; + +/** + * Retrieves the primary image URL for a given item. + * + * @param api - The Jellyfin API instance. + * @param item - The media item to retrieve the backdrop image URL for. + * @param quality - The desired image quality (default: 90). + */ +export const getParentBackdropImageUrl = ({ + api, + item, + quality = 80, + width = 400, +}: { + api?: Api | null; + item?: BaseItemDto | null; + quality?: number | null; + width?: number | null; +}) => { + if (!item || !api) { + return null; + } + + const parentId = item.ParentBackdropItemId; + const tag = item.ParentBackdropImageTags?.[0] || ""; + + const params = new URLSearchParams({ + fillWidth: width ? String(width) : "500", + quality: quality ? String(quality) : "80", + tag: tag, + }); + + return `${ + api?.basePath + }/Items/${parentId}/Images/Backdrop/0?${params.toString()}`; +}; diff --git a/utils/jellyfin/image/getPrimaryParentImageUrl.ts b/utils/jellyfin/image/getPrimaryParentImageUrl.ts new file mode 100644 index 00000000..ff862624 --- /dev/null +++ b/utils/jellyfin/image/getPrimaryParentImageUrl.ts @@ -0,0 +1,42 @@ +import { Api } from "@jellyfin/sdk"; +import { + BaseItemDto, + BaseItemPerson, +} from "@jellyfin/sdk/lib/generated-client/models"; +import { isBaseItemDto } from "../jellyfin"; + +/** + * Retrieves the primary image URL for a given item. + * + * @param api - The Jellyfin API instance. + * @param item - The media item to retrieve the backdrop image URL for. + * @param quality - The desired image quality (default: 90). + */ +export const getPrimaryParentImageUrl = ({ + api, + item, + quality = 80, + width = 400, +}: { + api?: Api | null; + item?: BaseItemDto | null; + quality?: number | null; + width?: number | null; +}) => { + if (!item || !api) { + return null; + } + + const parentId = item.ParentId; + const primaryTag = item.ParentPrimaryImageTag?.[0]; + + const params = new URLSearchParams({ + fillWidth: width ? String(width) : "500", + quality: quality ? String(quality) : "80", + tag: primaryTag || "", + }); + + return `${ + api?.basePath + }/Items/${parentId}/Images/Primary?${params.toString()}`; +};