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

@@ -1,16 +1,14 @@
import { FlashList, FlashListProps } from "@shopify/flash-list";
import React, { useEffect } from "react";
import React, { forwardRef, useImperativeHandle, useRef } from "react";
import { View, ViewStyle } from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
import { Loader } from "../Loader";
import { Text } from "./Text";
type PartialExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;
export interface HorizontalScrollRef {
scrollToIndex: (index: number, viewOffset: number) => void;
}
interface HorizontalScrollProps<T>
extends PartialExcept<
Omit<FlashListProps<T>, "renderItem">,
@@ -23,61 +21,69 @@ interface HorizontalScrollProps<T>
loadingContainerStyle?: ViewStyle;
height?: number;
loading?: boolean;
extraData?: any;
}
export function HorizontalScroll<T>({
data = [],
renderItem,
containerStyle,
contentContainerStyle,
loadingContainerStyle,
loading = false,
height = 164,
...props
}: HorizontalScrollProps<T>): React.ReactElement {
const animatedOpacity = useSharedValue(0);
const animatedStyle1 = useAnimatedStyle(() => {
return {
opacity: withTiming(animatedOpacity.value, { duration: 250 }),
};
});
export const HorizontalScroll = forwardRef<
HorizontalScrollRef,
HorizontalScrollProps<any>
>(
<T,>(
{
data = [],
renderItem,
containerStyle,
contentContainerStyle,
loadingContainerStyle,
loading = false,
height = 164,
extraData,
...props
}: HorizontalScrollProps<T>,
ref: React.ForwardedRef<HorizontalScrollRef>
) => {
const flashListRef = useRef<FlashList<T>>(null);
useEffect(() => {
if (data) {
animatedOpacity.value = 1;
}
}, [data]);
useImperativeHandle(ref!, () => ({
scrollToIndex: (index: number, viewOffset: number) => {
flashListRef.current?.scrollToIndex({
index,
animated: true,
viewPosition: 0,
viewOffset,
});
},
}));
if (data === undefined || data === null || loading) {
return (
<View
style={[
{
flex: 1,
justifyContent: "center",
alignItems: "center",
},
loadingContainerStyle,
]}
>
<Loader />
const renderFlashListItem = ({
item,
index,
}: {
item: T;
index: number;
}) => (
<View className="mr-2">
<React.Fragment>{renderItem(item, index)}</React.Fragment>
</View>
);
}
const renderFlashListItem = ({ item, index }: { item: T; index: number }) => (
<View className="mr-2">
<React.Fragment>{renderItem(item, index)}</React.Fragment>
</View>
);
if (!data || loading) {
return (
<View className="px-4 mb-2">
<View className="bg-neutral-950 h-24 w-full rounded-md mb-2"></View>
<View className="bg-neutral-950 h-10 w-full rounded-md mb-1"></View>
</View>
);
}
return (
<Animated.View style={[containerStyle, animatedStyle1]}>
<FlashList
return (
<FlashList<T>
ref={flashListRef}
data={data}
extraData={extraData}
renderItem={renderFlashListItem}
horizontal
estimatedItemSize={100}
estimatedItemSize={200}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{
paddingHorizontal: 16,
@@ -90,6 +96,6 @@ export function HorizontalScroll<T>({
)}
{...props}
/>
</Animated.View>
);
}
);
}
);

View File

@@ -0,0 +1,101 @@
import { View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { Image, ImageProps, ImageSource } from "expo-image";
import { useMemo, useState } from "react";
import { useAtom } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
interface Props extends ImageProps {
item: BaseItemDto;
variant?: "Backdrop" | "Primary" | "Thumb" | "Logo";
quality?: number;
width?: number;
}
export const ItemImage: React.FC<Props> = ({
item,
variant,
quality = 90,
width = 1000,
...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":
console.log("case Primary");
tag = item.ImageTags?.["Primary"];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Primary?.[tag];
console.log("bh: ", blurhash);
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}`,
blurhash,
};
break;
case "Thumb":
console.log("case Thumb");
tag = item.ImageTags?.["Thumb"];
if (!tag) break;
blurhash = item.ImageBlurHashes?.Thumb?.[tag];
console.log("bh: ", blurhash);
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Backdrop?quality=${quality}&tag=${tag}`,
blurhash,
};
break;
default:
console.log("case default");
tag = item.ImageTags?.["Primary"];
src = {
uri: `${api.basePath}/Items/${item.Id}/Images/Primary?quality=${quality}&tag=${tag}`,
};
break;
}
return src;
}, [item.ImageTags]);
return (
<Image
transition={300}
placeholder={{
blurhash: source?.blurhash,
}}
source={{
uri: source?.uri,
}}
{...props}
/>
);
};

View File

@@ -72,7 +72,7 @@ export const TouchableItemRouter: React.FC<PropsWithChildren<Props>> = ({
// return;
// }
router.push(`/(auth)/(tabs)/${from}/items/${item.Id}`);
router.push(`/(auth)/(tabs)/${from}/items/page?id=${item.Id}`);
}}
{...props}
>