wip: use general poster component

This commit is contained in:
Fredrik Burmester
2024-09-03 08:54:05 +03:00
parent 7b9bad630f
commit d4252682be
5 changed files with 159 additions and 89 deletions

View File

@@ -11,8 +11,10 @@ import { ItemImage } from "@/components/common/ItemImage";
import { CastAndCrew } from "@/components/series/CastAndCrew";
import { CurrentSeries } from "@/components/series/CurrentSeries";
import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarousel";
import { useImageColors } from "@/hooks/useImageColors";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getItemImage } from "@/utils/getItemImage";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
@@ -25,23 +27,22 @@ import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useNavigation } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation";
import { useAtom } from "jotai";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { View } from "react-native";
import { useCastDevice } from "react-native-google-cast";
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Chromecast } from "./Chromecast";
import { ItemHeader } from "./ItemHeader";
import { MediaSourceSelector } from "./MediaSourceSelector";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
runOnJS,
} from "react-native-reanimated";
import { Loader } from "./Loader";
import { set } from "lodash";
import * as ScreenOrientation from "expo-screen-orientation";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { MediaSourceSelector } from "./MediaSourceSelector";
export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
const [api] = useAtom(apiAtom);
@@ -61,7 +62,6 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
value: undefined,
});
const [loadingImage, setLoadingImage] = useState(true);
const [loadingLogo, setLoadingLogo] = useState(true);
const [orientation, setOrientation] = useState(
@@ -233,12 +233,22 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
});
const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]);
const themeImageColorSource = useMemo(() => {
if (!api || !item) return;
return getItemImage({
item,
api,
variant: "Primary",
quality: 80,
width: 300,
});
}, [api, item]);
useImageColors(themeImageColorSource?.uri);
const loading = useMemo(() => {
return Boolean(
isLoading || isFetching || loadingImage || (logoUrl && loadingLogo)
);
}, [isLoading, isFetching, loadingImage, loadingLogo, logoUrl]);
return Boolean(isLoading || isFetching || (logoUrl && loadingLogo));
}, [isLoading, isFetching, loadingLogo, logoUrl]);
const insets = useSafeAreaInsets();
@@ -263,6 +273,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
<Animated.View style={[animatedStyle, { flex: 1 }]}>
{localItem && (
<ItemImage
useThemeColor
variant={
localItem.Type === "Movie" && logoUrl
? "Backdrop"
@@ -273,8 +284,6 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
width: "100%",
height: "100%",
}}
onLoad={() => setLoadingImage(false)}
onError={() => setLoadingImage(false)}
/>
)}
</Animated.View>

View File

@@ -1,5 +1,6 @@
import { useImageColors } from "@/hooks/useImageColors";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
import { Ionicons } from "@expo/vector-icons";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image, ImageProps, ImageSource } from "expo-image";
@@ -21,6 +22,8 @@ interface Props extends ImageProps {
| "Thumb";
quality?: number;
width?: number;
useThemeColor?: boolean;
onError?: () => void;
}
export const ItemImage: React.FC<Props> = ({
@@ -28,80 +31,41 @@ export const ItemImage: React.FC<Props> = ({
variant = "Primary",
quality = 90,
width = 1000,
useThemeColor = false,
onError,
...props
}) => {
const [api] = useAtom(apiAtom);
const source = useMemo(() => {
if (!api) return null;
let tag: string | null | undefined;
let blurhash: string | null | undefined;
let src: ImageSource | null = null;
console.log("ImageItem ~ " + variant, item.Name, item.ImageTags);
switch (variant) {
case "Backdrop":
if (item.Type === "Episode") {
tag = item.ParentBackdropImageTags?.[0];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Backdrop?.[tag];
src = {
uri: `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Backdrop/0?quality=${quality}&tag=${tag}&width=${width}`,
blurhash,
};
break;
}
tag = item.BackdropImageTags?.[0];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Backdrop?.[tag];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Backdrop/0?quality=${quality}&tag=${tag}&width=${width}`,
blurhash,
};
break;
case "Primary":
tag = item.ImageTags?.["Primary"];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Primary?.[tag];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}&width=${width}`,
blurhash,
};
break;
case "Thumb":
tag = item.ImageTags?.["Thumb"];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Thumb?.[tag];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Backdrop?quality=${quality}&tag=${tag}&width=${width}`,
blurhash,
};
break;
default:
tag = item.ImageTags?.["Primary"];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}&width=${width}`,
};
break;
if (!api) {
onError && onError();
return;
}
console.log("src: ", src?.uri?.slice(0, 30));
return src;
}, [item.ImageTags]);
useImageColors(source?.uri);
return getItemImage({
item,
api,
variant,
quality,
width,
});
}, [api, item, quality, variant, width]);
// return placeholder icon if no source
if (!source?.uri) return;
<View {...props}>
<Ionicons name="image-outline" size={24} color="white" />;
</View>;
if (!source?.uri)
return (
<View
{...props}
className="flex flex-col items-center justify-center border border-neutral-800 bg-neutral-900"
>
<Ionicons
name="image-outline"
size={24}
color="white"
style={{ opacity: 0.4 }}
/>
</View>
);
return (
<Image

View File

@@ -24,7 +24,10 @@ export const ItemPoster: React.FC<Props> = ({
if (item.Type === "Movie" || item.Type === "Series" || item.Type === "BoxSet")
return (
<View className="relative rounded-lg overflow-hidden border border-neutral-900">
<View
className="relative rounded-lg overflow-hidden border border-neutral-900"
{...props}
>
<ItemImage
style={{
aspectRatio: "10/15",
@@ -40,7 +43,10 @@ export const ItemPoster: React.FC<Props> = ({
);
return (
<View className="rounded-lg w-full aspect-square overflow-hidden border border-neutral-900">
<View
className="rounded-lg w-full aspect-square overflow-hidden border border-neutral-900"
{...props}
>
<ItemImage className="w-full aspect-square" item={item} />
</View>
);

View File

@@ -1,12 +1,16 @@
import { useState, useEffect } from "react";
import { getColors } from "react-native-image-colors";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useAtom } from "jotai";
import { useEffect } from "react";
import { getColors } from "react-native-image-colors";
export const useImageColors = (uri: string | undefined | null) => {
export const useImageColors = (
uri: string | undefined | null,
disabled = false
) => {
const [, setPrimaryColor] = useAtom(itemThemeColorAtom);
useEffect(() => {
if (disabled) return;
if (uri) {
getColors(uri, {
fallback: "#fff",
@@ -38,5 +42,5 @@ export const useImageColors = (uri: string | undefined | null) => {
console.error("Error getting colors", error);
});
}
}, [uri, setPrimaryColor]);
}, [uri, setPrimaryColor, disabled]);
};

87
utils/getItemImage.ts Normal file
View File

@@ -0,0 +1,87 @@
import { Api } from "@jellyfin/sdk";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { ImageSource } from "expo-image";
interface Props {
item: BaseItemDto;
api: Api;
quality?: number;
width?: number;
variant?:
| "Primary"
| "Backdrop"
| "ParentBackdrop"
| "ParentLogo"
| "Logo"
| "AlbumPrimary"
| "SeriesPrimary"
| "Screenshot"
| "Thumb";
}
export const getItemImage = ({
item,
api,
variant = "Primary",
quality = 90,
width = 1000,
}: Props) => {
if (!api) return null;
let tag: string | null | undefined;
let blurhash: string | null | undefined;
let src: ImageSource | null = null;
switch (variant) {
case "Backdrop":
if (item.Type === "Episode") {
tag = item.ParentBackdropImageTags?.[0];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Backdrop?.[tag];
src = {
uri: `${api.basePath}/Items/${item.ParentBackdropItemId}/Images/Backdrop/0?quality=${quality}&tag=${tag}&width=${width}`,
blurhash,
};
break;
}
tag = item.BackdropImageTags?.[0];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Backdrop?.[tag];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Backdrop/0?quality=${quality}&tag=${tag}&width=${width}`,
blurhash,
};
break;
case "Primary":
tag = item.ImageTags?.["Primary"];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Primary?.[tag];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}&width=${width}`,
blurhash,
};
break;
case "Thumb":
tag = item.ImageTags?.["Thumb"];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Thumb?.[tag];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Backdrop?quality=${quality}&tag=${tag}&width=${width}`,
blurhash,
};
break;
default:
tag = item.ImageTags?.["Primary"];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}&width=${width}`,
};
break;
}
if (!src?.uri) return null;
return src;
};