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()}`;
+};