mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
fix: posters
This commit is contained in:
@@ -222,7 +222,7 @@ export default function index() {
|
||||
})
|
||||
).data.Items || [],
|
||||
type: "ScrollingCollectionList",
|
||||
orientation: "vertical",
|
||||
orientation: "horizontal",
|
||||
},
|
||||
];
|
||||
return ss;
|
||||
|
||||
@@ -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={
|
||||
<>
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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="">
|
||||
|
||||
@@ -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/${
|
||||
|
||||
@@ -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()}`;
|
||||
};
|
||||
|
||||
42
utils/jellyfin/image/getParentBackdropImageUrl.ts
Normal file
42
utils/jellyfin/image/getParentBackdropImageUrl.ts
Normal 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()}`;
|
||||
};
|
||||
42
utils/jellyfin/image/getPrimaryParentImageUrl.ts
Normal file
42
utils/jellyfin/image/getPrimaryParentImageUrl.ts
Normal 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()}`;
|
||||
};
|
||||
Reference in New Issue
Block a user