diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index 75ae50b8..ac25bacb 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -109,7 +109,7 @@ export default function index() { return response.data.Items || []; }, enabled: !!api && !!user?.Id && settings?.usePopularPlugin === true, - staleTime: 0, + staleTime: 60 * 1000, }); const movieCollectionId = useMemo(() => { @@ -148,6 +148,7 @@ export default function index() { ( await getItemsApi(api).getResumeItems({ userId: user.Id, + enableImageTypes: ["Primary", "Backdrop", "Thumb"], }) ).data.Items || [], type: "ScrollingCollectionList", @@ -162,6 +163,7 @@ export default function index() { userId: user?.Id, fields: ["MediaSourceCount"], limit: 20, + enableImageTypes: ["Primary", "Backdrop", "Thumb"], }) ).data.Items || [], type: "ScrollingCollectionList", @@ -220,7 +222,7 @@ export default function index() { }) ).data.Items || [], type: "ScrollingCollectionList", - orientation: "horizontal", + orientation: "vertical", }, ]; 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 84958a90..e62f619a 100644 --- a/app/(auth)/(tabs)/(home,libraries,search)/items/[id].tsx +++ b/app/(auth)/(tabs)/(home,libraries,search)/items/[id].tsx @@ -62,7 +62,7 @@ const page: React.FC = () => { itemId: id, }), enabled: !!id && !!api, - staleTime: 60, + staleTime: 60 * 1000, }); const { data: sessionData } = useQuery({ @@ -130,8 +130,8 @@ const page: React.FC = () => { getBackdropUrl({ api, item, - quality: 90, - width: 1000, + quality: 95, + width: 1200, }), [item] ); @@ -227,16 +227,11 @@ const page: React.FC = () => { - - - - {item.Type === "Episode" && ( - - - - )} - - + + + {item.Type === "Episode" && } + + diff --git a/components/Button.tsx b/components/Button.tsx index 813c4222..305312d4 100644 --- a/components/Button.tsx +++ b/components/Button.tsx @@ -10,7 +10,7 @@ interface ButtonProps extends React.ComponentProps { disabled?: boolean; children?: string | ReactNode; loading?: boolean; - color?: "purple" | "red" | "black"; + color?: "purple" | "red" | "black" | "transparent"; iconRight?: ReactNode; iconLeft?: ReactNode; justify?: "center" | "between"; @@ -37,6 +37,8 @@ export const Button: React.FC> = ({ return "bg-red-600"; case "black": return "bg-neutral-900 border border-neutral-800"; + case "transparent": + return "bg-transparent"; } }, [color]); diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index e057ee15..20acb45b 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -6,6 +6,8 @@ import { useMemo, useState } from "react"; import { View } from "react-native"; import { WatchedIndicator } from "./WatchedIndicator"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; +import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; +import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; type ContinueWatchingPosterProps = { item: BaseItemDto; @@ -20,12 +22,7 @@ const ContinueWatchingPoster: React.FC = ({ const url = useMemo( () => - getPrimaryImageUrl({ - api, - item, - quality: 80, - width: 300, - }), + `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`, [item] ); diff --git a/components/SimilarItems.tsx b/components/SimilarItems.tsx index 45624a52..f0d6ff40 100644 --- a/components/SimilarItems.tsx +++ b/components/SimilarItems.tsx @@ -6,16 +6,19 @@ import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; import { useAtom } from "jotai"; import { useMemo } from "react"; -import { ScrollView, TouchableOpacity, View } from "react-native"; +import { ScrollView, TouchableOpacity, View, ViewProps } from "react-native"; import { Text } from "./common/Text"; import { ItemCardText } from "./ItemCardText"; import { Loader } from "./Loader"; -type SimilarItemsProps = { +interface SimilarItemsProps extends ViewProps { itemId: string; -}; +} -export const SimilarItems: React.FC = ({ itemId }) => { +export const SimilarItems: React.FC = ({ + itemId, + ...props +}) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); @@ -41,8 +44,8 @@ export const SimilarItems: React.FC = ({ itemId }) => { ); return ( - - Similar items + + Similar items {isLoading ? ( diff --git a/components/home/ScrollingCollectionList.tsx b/components/home/ScrollingCollectionList.tsx index df86bb88..3a5f3dc5 100644 --- a/components/home/ScrollingCollectionList.tsx +++ b/components/home/ScrollingCollectionList.tsx @@ -11,6 +11,8 @@ import { useQuery, type QueryFunction, } from "@tanstack/react-query"; +import SeriesPoster from "../posters/SeriesPoster"; +import { EpisodePoster } from "../posters/EpisodePoster"; interface Props extends ViewProps { title?: string | null; @@ -34,7 +36,7 @@ export const ScrollingCollectionList: React.FC = ({ queryKey, queryFn, enabled: !disabled, - staleTime: 0, + staleTime: 60 * 1000, }); if (disabled || !title) return null; @@ -53,15 +55,18 @@ export const ScrollingCollectionList: React.FC = ({ key={index} item={item} className={`flex flex-col - ${orientation === "vertical" ? "w-28" : "w-44"} + ${orientation === "horizontal" ? "w-44" : "w-28"} `} > - {orientation === "vertical" ? ( - - ) : ( + {item.Type === "Episode" && orientation === "horizontal" && ( )} + {item.Type === "Episode" && orientation === "vertical" && ( + + )} + {item.Type === "Movie" && } + {item.Type === "Series" && } diff --git a/components/posters/EpisodePoster.tsx b/components/posters/EpisodePoster.tsx new file mode 100644 index 00000000..c82464d5 --- /dev/null +++ b/components/posters/EpisodePoster.tsx @@ -0,0 +1,64 @@ +import { apiAtom } from "@/providers/JellyfinProvider"; +import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { Image } from "expo-image"; +import { useAtom } from "jotai"; +import { useMemo, useState } from "react"; +import { View } from "react-native"; +import { WatchedIndicator } from "@/components/WatchedIndicator"; + +type MoviePosterProps = { + item: BaseItemDto; + showProgress?: boolean; +}; + +export const EpisodePoster: React.FC = ({ + item, + showProgress = false, +}) => { + const [api] = useAtom(apiAtom); + + const url = useMemo(() => { + if (item.Type === "Episode") { + return `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`; + } + }, [item]); + + const [progress, setProgress] = useState( + item.UserData?.PlayedPercentage || 0 + ); + + const blurhash = useMemo(() => { + const key = item.ImageTags?.["Primary"] as string; + return item.ImageBlurHashes?.["Primary"]?.[key]; + }, [item]); + + return ( + + + + {showProgress && progress > 0 && ( + + )} + + ); +}; diff --git a/components/posters/MoviePoster.tsx b/components/posters/MoviePoster.tsx index 972392b6..ff9f3458 100644 --- a/components/posters/MoviePoster.tsx +++ b/components/posters/MoviePoster.tsx @@ -18,15 +18,16 @@ const MoviePoster: React.FC = ({ }) => { const [api] = useAtom(apiAtom); - const url = useMemo( - () => - getPrimaryImageUrl({ - api, - item, - width: 300, - }), - [item] - ); + const url = useMemo(() => { + if (item.Type === "Episode") { + return `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ParentThumbImageTag}`; + } + return getPrimaryImageUrl({ + api, + item, + width: 300, + }); + }, [item]); const [progress, setProgress] = useState( item.UserData?.PlayedPercentage || 0 diff --git a/components/posters/SeriesPoster.tsx b/components/posters/SeriesPoster.tsx index d235639f..dbadcdce 100644 --- a/components/posters/SeriesPoster.tsx +++ b/components/posters/SeriesPoster.tsx @@ -15,14 +15,16 @@ type MoviePosterProps = { const SeriesPoster: React.FC = ({ item }) => { const [api] = useAtom(apiAtom); - const url = useMemo( - () => - getPrimaryImageUrl({ - api, - item, - }), - [item] - ); + const url = useMemo(() => { + if (item.Type === "Episode") { + return `${api?.basePath}/Items/${item.SeriesId}/Images/Primary?fillHeight=389&quality=80&tag=${item.SeriesPrimaryImageTag}`; + } + return getPrimaryImageUrl({ + api, + item, + width: 300, + }); + }, [item]); const blurhash = useMemo(() => { const key = item.ImageTags?.["Primary"] as string; diff --git a/components/series/CastAndCrew.tsx b/components/series/CastAndCrew.tsx index ab041bd7..d0406ce6 100644 --- a/components/series/CastAndCrew.tsx +++ b/components/series/CastAndCrew.tsx @@ -1,27 +1,26 @@ +import { apiAtom } from "@/providers/JellyfinProvider"; +import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; import { BaseItemDto, BaseItemPerson, } from "@jellyfin/sdk/lib/generated-client/models"; +import { router } from "expo-router"; +import { useAtom } from "jotai"; import React from "react"; -import { Linking, TouchableOpacity, View } from "react-native"; +import { TouchableOpacity, View, ViewProps } from "react-native"; import { HorizontalScroll } from "../common/HorrizontalScroll"; import { Text } from "../common/Text"; import Poster from "../posters/Poster"; -import { useAtom } from "jotai"; -import { apiAtom } from "@/providers/JellyfinProvider"; -import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; -import { router, usePathname } from "expo-router"; -import { useSettings } from "@/utils/atoms/settings"; -export const CastAndCrew = ({ item }: { item: BaseItemDto }) => { +interface Props extends ViewProps { + item: BaseItemDto; +} + +export const CastAndCrew: React.FC = ({ item, ...props }) => { const [api] = useAtom(apiAtom); - const [settings] = useSettings(); - - const pathname = usePathname(); - return ( - + Cast & Crew > data={item.People} diff --git a/components/series/CurrentSeries.tsx b/components/series/CurrentSeries.tsx index a71dea8d..8d06d2e7 100644 --- a/components/series/CurrentSeries.tsx +++ b/components/series/CurrentSeries.tsx @@ -3,17 +3,21 @@ import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { router } from "expo-router"; import { useAtom } from "jotai"; import React from "react"; -import { TouchableOpacity, View } from "react-native"; +import { TouchableOpacity, View, ViewProps } from "react-native"; import Poster from "../posters/Poster"; import { HorizontalScroll } from "../common/HorrizontalScroll"; import { Text } from "../common/Text"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; -export const CurrentSeries = ({ item }: { item: BaseItemDto }) => { +interface Props extends ViewProps { + item: BaseItemDto; +} + +export const CurrentSeries: React.FC = ({ item, ...props }) => { const [api] = useAtom(apiAtom); return ( - + Series data={[item]} diff --git a/components/series/NextUp.tsx b/components/series/NextUp.tsx index e4ac462d..fa8558fb 100644 --- a/components/series/NextUp.tsx +++ b/components/series/NextUp.tsx @@ -34,7 +34,7 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => { if (!items?.length) return ( - + Next up No items to display