fix: posters

This commit is contained in:
Fredrik Burmester
2024-08-26 08:30:12 +02:00
parent d962507749
commit c12b58e5cb
10 changed files with 179 additions and 29 deletions

View File

@@ -222,7 +222,7 @@ export default function index() {
})
).data.Items || [],
type: "ScrollingCollectionList",
orientation: "vertical",
orientation: "horizontal",
},
];
return ss;

View File

@@ -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 = () => {
</View>
);
if (!item?.Id || !backdropUrl) return null;
if (!item?.Id) return null;
return (
<ParallaxScrollView
headerHeight={item.Type === "Episode" ? 300 : 400}
headerImage={
<Image
source={{
uri: backdropUrl,
}}
style={{
width: "100%",
height: "100%",
}}
/>
<>
{itemBackdropUrl ? (
<Image
source={{
uri: itemBackdropUrl,
}}
style={{
width: "100%",
height: "100%",
}}
/>
) : null}
</>
}
logo={
<>

View File

@@ -9,11 +9,13 @@ import { WatchedIndicator } from "./WatchedIndicator";
type ContinueWatchingPosterProps = {
item: BaseItemDto;
width?: number;
useEpisodePoster?: boolean;
};
const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
item,
width = 176,
useEpisodePoster = false,
}) => {
const [api] = useAtom(apiAtom);
@@ -22,6 +24,9 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
*/
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}`;

View File

@@ -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<Props> = ({
children,
headerImage,
episodePoster,
headerHeight = 400,
logo,
}: Props) => {
const scrollRef = useAnimatedRef<Animated.ScrollView>();
@@ -29,14 +31,14 @@ export const ParallaxScrollView: React.FC<Props> = ({
{
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<Props> = ({
scrollEventThrottle={16}
>
{logo && (
<View className="absolute top-[250px] h-[130px] left-0 w-full z-40 px-4 flex justify-center items-center">
<View
style={{
top: headerHeight - 150,
height: 130,
}}
className="absolute left-0 w-full z-40 px-4 flex justify-center items-center"
>
{logo}
</View>
)}
{episodePoster && (
<View className="absolute top-[290px] h-[120px] w-full left-0 flex justify-center items-center z-50">
<View className="h-full aspect-video border border-neutral-800">
{episodePoster}
</View>
</View>
)}
<Animated.View
style={[
{
height: HEADER_HEIGHT,
height: headerHeight,
backgroundColor: "black",
},
headerAnimatedStyle,

View File

@@ -53,7 +53,7 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
key={item.Id}
className="flex flex-col w-44"
>
<ContinueWatchingPoster item={item} />
<ContinueWatchingPoster item={item} useEpisodePoster />
<ItemCardText item={item} />
</TouchableOpacity>
)}

View File

@@ -188,7 +188,11 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
>
<View className="flex flex-row items-center mb-2">
<View className="w-32 aspect-video overflow-hidden mr-2">
<ContinueWatchingPoster item={e} width={128} />
<ContinueWatchingPoster
item={e}
width={128}
useEpisodePoster
/>
</View>
<View className="shrink">
<Text numberOfLines={2} className="">

View File

@@ -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/${

View File

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

View File

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

View File

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