fix: better posters and item screen

This commit is contained in:
Fredrik Burmester
2024-08-26 19:47:02 +02:00
parent 07c5c21599
commit 3047367ba6
29 changed files with 534 additions and 302 deletions

View File

@@ -13,17 +13,19 @@ import { Text } from "../common/Text";
import Poster from "../posters/Poster";
interface Props extends ViewProps {
item: BaseItemDto;
item?: BaseItemDto | null;
loading?: boolean;
}
export const CastAndCrew: React.FC<Props> = ({ item, ...props }) => {
export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
const [api] = useAtom(apiAtom);
return (
<View {...props}>
<View {...props} className="flex flex-col">
<Text className="text-lg font-bold mb-2 px-4">Cast & Crew</Text>
<HorizontalScroll<NonNullable<BaseItemPerson>>
data={item.People}
<HorizontalScroll
loading={loading}
data={item?.People || []}
renderItem={(item, index) => (
<TouchableOpacity
onPress={() => {

View File

@@ -10,7 +10,7 @@ import { Text } from "../common/Text";
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
interface Props extends ViewProps {
item: BaseItemDto;
item?: BaseItemDto | null;
}
export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
@@ -19,7 +19,7 @@ export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
return (
<View {...props}>
<Text className="text-lg font-bold mb-2 px-4">Series</Text>
<HorizontalScroll<BaseItemDto>
<HorizontalScroll
data={[item]}
renderItem={(item, index) => (
<TouchableOpacity

View File

@@ -0,0 +1,40 @@
import { Text } from "@/components/common/Text";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useRouter } from "expo-router";
import { TouchableOpacity, View, ViewProps } from "react-native";
interface Props extends ViewProps {
item: BaseItemDto;
}
export const EpisodeTitleHeader: React.FC<Props> = ({ item, ...props }) => {
const router = useRouter();
return (
<View {...props} className="flex flex-col">
<TouchableOpacity
onPress={() => router.push(`/(auth)/series/${item.SeriesId}`)}
>
<Text className="text-center opacity-50">{item?.SeriesName}</Text>
</TouchableOpacity>
<Text className="text-center font-bold text-2xl">{item?.Name}</Text>
<View className="flex flex-row items-center self-center">
<TouchableOpacity
onPress={() => {
router.push(
// @ts-ignore
`/(auth)/series/${item.SeriesId}?seasonIndex=${item?.ParentIndexNumber}`
);
}}
>
<Text className="text-center opacity-50">{item?.SeasonName}</Text>
</TouchableOpacity>
<Text className="text-center opacity-50 mx-2">{"—"}</Text>
<Text className="text-center opacity-50">
{`Episode ${item.IndexNumber}`}
</Text>
</View>
<Text className="text-center opacity-50">{item?.ProductionYear}</Text>
</View>
);
};

View File

@@ -23,40 +23,6 @@ export const NextEpisodeButton: React.FC<Props> = ({
const [user] = useAtom(userAtom);
const [api] = useAtom(apiAtom);
// const { data: seasons } = useQuery({
// queryKey: ["seasons", item.SeriesId],
// queryFn: async () => {
// if (
// !api ||
// !user?.Id ||
// !item?.Id ||
// !item?.SeriesId ||
// !item?.IndexNumber
// )
// return [];
// const response = await getItemsApi(api).getItems({
// parentId: item?.SeriesId,
// });
// console.log("seasons ~", type, response.data);
// return (response.data.Items as BaseItemDto[]) ?? [];
// },
// enabled: Boolean(api && user?.Id && item?.Id && item.SeasonId),
// });
// const nextSeason = useMemo(() => {
// if (!seasons) return null;
// const currentSeasonIndex = seasons.findIndex(
// (season) => season.Id === item.SeasonId,
// );
// if (currentSeasonIndex === seasons.length - 1) return null;
// return seasons[currentSeasonIndex + 1];
// }, [seasons]);
const { data: nextEpisode } = useQuery({
queryKey: ["nextEpisode", item.Id, item.ParentId, type],
queryFn: async () => {
@@ -90,7 +56,7 @@ export const NextEpisodeButton: React.FC<Props> = ({
return (
<Button
onPress={() => router.push(`/items/${nextEpisode?.Id}`)}
onPress={() => router.setParams({ id: nextEpisode?.Id })}
className={`h-12 aspect-square`}
disabled={disabled}
{...props}

View File

@@ -43,12 +43,12 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
return (
<View>
<Text className="text-lg font-bold mb-2 px-4">Next up</Text>
<HorizontalScroll<BaseItemDto>
<HorizontalScroll
data={items}
renderItem={(item, index) => (
<TouchableOpacity
onPress={() => {
router.push(`/(auth)/items/${item.Id}`);
router.push(`/(auth)/items/page?id=${item.Id}`);
}}
key={item.Id}
className="flex flex-col w-44"

View File

@@ -0,0 +1,146 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { router } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useMemo, useRef } from "react";
import { TouchableOpacity, View, ViewProps } from "react-native";
import {
HorizontalScroll,
HorizontalScrollRef,
} from "../common/HorrizontalScroll";
import ContinueWatchingPoster from "../ContinueWatchingPoster";
import { ItemCardText } from "../ItemCardText";
interface Props extends ViewProps {
item?: BaseItemDto | null;
loading?: boolean;
}
export const SeasonEpisodesCarousel: React.FC<Props> = ({
item,
loading,
...props
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const scrollRef = useRef<HorizontalScrollRef>(null);
const scrollToIndex = (index: number) => {
scrollRef.current?.scrollToIndex(index, 16);
};
const seasonId = useMemo(() => {
return item?.SeasonId;
}, [item]);
const {
data: episodes,
isLoading,
isFetching,
} = useQuery({
queryKey: ["episodes", seasonId],
queryFn: async () => {
if (!api || !user?.Id) return [];
const response = await api.axiosInstance.get(
`${api.basePath}/Shows/${item?.Id}/Episodes`,
{
params: {
userId: user?.Id,
seasonId,
Fields:
"ItemCounts,PrimaryImageAspectRatio,CanDelete,MediaSourceCount,Overview",
},
headers: {
Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
},
}
);
return response.data.Items as BaseItemDto[];
},
enabled: !!api && !!user?.Id && !!seasonId,
});
/**
* Prefetch previous and next episode
*/
const queryClient = useQueryClient();
useEffect(() => {
if (!item?.Id || !item.IndexNumber || !episodes || episodes.length === 0) {
return;
}
const previousId = episodes?.find(
(ep) => ep.IndexNumber === item.IndexNumber! - 1
)?.Id;
if (previousId) {
queryClient.prefetchQuery({
queryKey: ["item", previousId],
queryFn: async () =>
await getUserItemData({
api,
userId: user?.Id,
itemId: previousId,
}),
staleTime: 60 * 1000,
});
}
const nextId = episodes?.find(
(ep) => ep.IndexNumber === item.IndexNumber! + 1
)?.Id;
if (nextId) {
queryClient.prefetchQuery({
queryKey: ["item", nextId],
queryFn: async () =>
await getUserItemData({
api,
userId: user?.Id,
itemId: nextId,
}),
staleTime: 60 * 1000,
});
}
}, [episodes, api, user?.Id, item]);
useEffect(() => {
if (item?.Type === "Episode") {
const index = episodes?.findIndex((ep) => ep.Id === item.Id);
if (index !== undefined && index !== -1) {
console.log("Scrolling to index:", index);
setTimeout(() => {
scrollToIndex(index);
}, 400);
} else {
console.log("Episode not found in the list:", item.Id);
}
}
}, [episodes, item]);
return (
<HorizontalScroll
ref={scrollRef}
data={episodes}
extraData={item}
loading={loading || isLoading || isFetching}
renderItem={(_item, idx) => (
<TouchableOpacity
key={_item.Id}
onPress={() => {
router.setParams({ id: _item.Id });
}}
className={`flex flex-col w-44
${item?.Id === _item.Id ? "" : "opacity-50"}
`}
>
<ContinueWatchingPoster item={_item} useEpisodePoster />
<ItemCardText item={_item} />
</TouchableOpacity>
)}
{...props}
/>
);
};

View File

@@ -168,7 +168,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
{/* Old View. Might have a setting later to manually select view. */}
{/* {episodes && (
<View className="mt-4">
<HorizontalScroll<BaseItemDto>
<HorizontalScroll
data={episodes}
renderItem={(item, index) => (
<TouchableOpacity
@@ -200,7 +200,7 @@ export const SeasonPicker: React.FC<Props> = ({ item, initialSeasonIndex }) => {
<TouchableOpacity
key={e.Id}
onPress={() => {
router.push(`/(auth)/items/${e.Id}`);
router.push(`/(auth)/items/page?id=${e.Id}`);
}}
className="flex flex-col mb-4"
>

View File

@@ -1,44 +0,0 @@
import { TouchableOpacity, View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { useRouter } from "expo-router";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
interface Props extends ViewProps {
item: BaseItemDto;
}
export const SeriesTitleHeader: React.FC<Props> = ({ item, ...props }) => {
const router = useRouter();
return (
<>
<TouchableOpacity
onPress={() => router.push(`/(auth)/series/${item.SeriesId}`)}
>
<Text className="text-center opacity-50">{item?.SeriesName}</Text>
</TouchableOpacity>
<View className="flex flex-row items-center self-center px-4">
<Text className="text-center font-bold text-2xl mr-2">
{item?.Name}
</Text>
</View>
<View>
<View className="flex flex-row items-center self-center">
<TouchableOpacity
onPress={() => {
router.push(
// @ts-ignore
`/(auth)/series/${item.SeriesId}?seasonIndex=${item?.ParentIndexNumber}`
);
}}
>
<Text className="text-center opacity-50">{item?.SeasonName}</Text>
</TouchableOpacity>
<Text className="text-center opacity-50 mx-2">{"—"}</Text>
<Text className="text-center opacity-50">
{`Episode ${item.IndexNumber}`}
</Text>
</View>
</View>
</>
);
};