fix: design and posters

This commit is contained in:
Fredrik Burmester
2024-08-25 16:59:24 +02:00
parent b1726962c1
commit 8d327e8835
12 changed files with 140 additions and 66 deletions

View File

@@ -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;

View File

@@ -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 = () => {
<NextEpisodeButton item={item} className="ml-2" />
</View>
</View>
<CastAndCrew item={item} />
{item.Type === "Episode" && (
<View className="mb-4">
<CurrentSeries item={item} />
</View>
)}
<SimilarItems itemId={item.Id} />
<View className="flex flex-col space-y-4">
<CastAndCrew item={item} />
{item.Type === "Episode" && <CurrentSeries item={item} />}
<SimilarItems itemId={item.Id} />
</View>
<View className="h-12"></View>
</ParallaxScrollView>

View File

@@ -10,7 +10,7 @@ interface ButtonProps extends React.ComponentProps<typeof TouchableOpacity> {
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<PropsWithChildren<ButtonProps>> = ({
return "bg-red-600";
case "black":
return "bg-neutral-900 border border-neutral-800";
case "transparent":
return "bg-transparent";
}
}, [color]);

View File

@@ -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<ContinueWatchingPosterProps> = ({
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]
);

View File

@@ -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<SimilarItemsProps> = ({ itemId }) => {
export const SimilarItems: React.FC<SimilarItemsProps> = ({
itemId,
...props
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -41,8 +44,8 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({ itemId }) => {
);
return (
<View>
<Text className="px-4 text-2xl font-bold mb-2">Similar items</Text>
<View {...props}>
<Text className="px-4 text-lg font-bold mb-2">Similar items</Text>
{isLoading ? (
<View className="my-12">
<Loader />

View File

@@ -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<Props> = ({
queryKey,
queryFn,
enabled: !disabled,
staleTime: 0,
staleTime: 60 * 1000,
});
if (disabled || !title) return null;
@@ -53,15 +55,18 @@ export const ScrollingCollectionList: React.FC<Props> = ({
key={index}
item={item}
className={`flex flex-col
${orientation === "vertical" ? "w-28" : "w-44"}
${orientation === "horizontal" ? "w-44" : "w-28"}
`}
>
<View>
{orientation === "vertical" ? (
<MoviePoster item={item} />
) : (
{item.Type === "Episode" && orientation === "horizontal" && (
<ContinueWatchingPoster item={item} />
)}
{item.Type === "Episode" && orientation === "vertical" && (
<SeriesPoster item={item} />
)}
{item.Type === "Movie" && <MoviePoster item={item} />}
{item.Type === "Series" && <MoviePoster item={item} />}
<ItemCardText item={item} />
</View>
</TouchableItemRouter>

View File

@@ -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<MoviePosterProps> = ({
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 (
<View className="relative rounded-lg overflow-hidden border border-neutral-900">
<Image
placeholder={{
blurhash,
}}
key={item.Id}
id={item.Id}
source={
url
? {
uri: url,
}
: null
}
cachePolicy={"memory-disk"}
contentFit="cover"
style={{
aspectRatio: "10/15",
width: "100%",
}}
/>
<WatchedIndicator item={item} />
{showProgress && progress > 0 && (
<View className="h-1 bg-red-600 w-full"></View>
)}
</View>
);
};

View File

@@ -18,15 +18,16 @@ const MoviePoster: React.FC<MoviePosterProps> = ({
}) => {
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

View File

@@ -15,14 +15,16 @@ type MoviePosterProps = {
const SeriesPoster: React.FC<MoviePosterProps> = ({ 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;

View File

@@ -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<Props> = ({ item, ...props }) => {
const [api] = useAtom(apiAtom);
const [settings] = useSettings();
const pathname = usePathname();
return (
<View>
<View {...props}>
<Text className="text-lg font-bold mb-2 px-4">Cast & Crew</Text>
<HorizontalScroll<NonNullable<BaseItemPerson>>
data={item.People}

View File

@@ -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<Props> = ({ item, ...props }) => {
const [api] = useAtom(apiAtom);
return (
<View>
<View {...props}>
<Text className="text-lg font-bold mb-2 px-4">Series</Text>
<HorizontalScroll<BaseItemDto>
data={[item]}

View File

@@ -34,7 +34,7 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
if (!items?.length)
return (
<View>
<View className="px-4">
<Text className="text-lg font-bold mb-2">Next up</Text>
<Text className="opacity-50">No items to display</Text>
</View>