diff --git a/app/(auth)/(tabs)/(home,libraries,search)/collections/[collectionId].tsx b/app/(auth)/(tabs)/(home,libraries,search)/collections/[collectionId].tsx
index b766391e..0b34914f 100644
--- a/app/(auth)/(tabs)/(home,libraries,search)/collections/[collectionId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search)/collections/[collectionId].tsx
@@ -1,8 +1,10 @@
+import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { FilterButton } from "@/components/filters/FilterButton";
import { ResetFiltersButton } from "@/components/filters/ResetFiltersButton";
import { ItemCardText } from "@/components/ItemCardText";
+import { ItemPoster } from "@/components/posters/ItemPoster";
import MoviePoster from "@/components/posters/MoviePoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import {
@@ -195,7 +197,8 @@ const page: React.FC = () => {
width: "89%",
}}
>
-
+
+ {/* */}
diff --git a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
index db6de866..7e6b23f2 100644
--- a/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
+++ b/app/(auth)/(tabs)/(libraries)/[libraryId].tsx
@@ -36,6 +36,7 @@ import {
} from "@jellyfin/sdk/lib/utils/api";
import { FlashList } from "@shopify/flash-list";
import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { ItemPoster } from "@/components/posters/ItemPoster";
const MemoizedTouchableItemRouter = React.memo(TouchableItemRouter);
@@ -187,7 +188,8 @@ const Page = () => {
width: "89%",
}}
>
-
+ {/* */}
+
diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx
index 2722e012..2bdb18db 100644
--- a/components/ItemContent.tsx
+++ b/components/ItemContent.tsx
@@ -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 }) => {
{localItem && (
= React.memo(({ id }) => {
width: "100%",
height: "100%",
}}
- onLoad={() => setLoadingImage(false)}
- onError={() => setLoadingImage(false)}
/>
)}
diff --git a/components/common/ItemImage.tsx b/components/common/ItemImage.tsx
index c3ad2ed6..bf4c0f1a 100644
--- a/components/common/ItemImage.tsx
+++ b/components/common/ItemImage.tsx
@@ -1,93 +1,83 @@
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";
import { useAtom } from "jotai";
import { useMemo } from "react";
+import { View } from "react-native";
interface Props extends ImageProps {
item: BaseItemDto;
- variant?: "Backdrop" | "Primary" | "Thumb" | "Logo";
+ variant?:
+ | "Primary"
+ | "Backdrop"
+ | "ParentBackdrop"
+ | "ParentLogo"
+ | "Logo"
+ | "AlbumPrimary"
+ | "SeriesPrimary"
+ | "Screenshot"
+ | "Thumb";
quality?: number;
width?: number;
+ useThemeColor?: boolean;
+ onError?: () => void;
}
export const ItemImage: React.FC = ({
item,
- variant,
+ 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;
-
- 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}`,
- 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}`,
- 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}`,
- 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}`,
- blurhash,
- };
- break;
- default:
- tag = item.ImageTags?.["Primary"];
- src = {
- uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}`,
- };
- break;
+ if (!api) {
+ onError && onError();
+ return;
}
+ return getItemImage({
+ item,
+ api,
+ variant,
+ quality,
+ width,
+ });
+ }, [api, item, quality, variant, width]);
- return src;
- }, [item.ImageTags]);
-
- useImageColors(source?.uri);
+ // return placeholder icon if no source
+ if (!source?.uri)
+ return (
+
+
+
+ );
return (
> = ({
}
if (item.Type === "UserView") {
- Alert.alert("Not implemented");
+ router.push(`/(auth)/(tabs)/${from}/collections/${item.Id}`);
return;
}
diff --git a/components/posters/ItemPoster.tsx b/components/posters/ItemPoster.tsx
new file mode 100644
index 00000000..86575ab9
--- /dev/null
+++ b/components/posters/ItemPoster.tsx
@@ -0,0 +1,53 @@
+import { View, ViewProps } from "react-native";
+import { Text } from "@/components/common/Text";
+import {
+ BaseItemDto,
+ BaseItemKind,
+} from "@jellyfin/sdk/lib/generated-client/models";
+import { ItemImage } from "../common/ItemImage";
+import { WatchedIndicator } from "../WatchedIndicator";
+import { useState } from "react";
+
+interface Props extends ViewProps {
+ item: BaseItemDto;
+ showProgress?: boolean;
+}
+
+export const ItemPoster: React.FC = ({
+ item,
+ showProgress,
+ ...props
+}) => {
+ const [progress, setProgress] = useState(
+ item.UserData?.PlayedPercentage || 0
+ );
+
+ if (item.Type === "Movie" || item.Type === "Series" || item.Type === "BoxSet")
+ return (
+
+
+
+ {showProgress && progress > 0 && (
+
+ )}
+
+ );
+
+ return (
+
+
+
+ );
+};
diff --git a/hooks/useImageColors.ts b/hooks/useImageColors.ts
index 7d93e8ce..7f15b4b0 100644
--- a/hooks/useImageColors.ts
+++ b/hooks/useImageColors.ts
@@ -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]);
};
diff --git a/utils/getItemImage.ts b/utils/getItemImage.ts
new file mode 100644
index 00000000..d106b0cf
--- /dev/null
+++ b/utils/getItemImage.ts
@@ -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;
+};