refactor(tv): use shared components and proper typography in actor page

This commit is contained in:
Fredrik Burmester
2026-01-28 22:51:35 +01:00
parent 409629bb4a
commit 94ac458f52

View File

@@ -19,17 +19,19 @@ import {
Dimensions,
Easing,
FlatList,
Pressable,
ScrollView,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "@/components/common/Text";
import { getItemNavigation } from "@/components/common/TouchableItemRouter";
import { ItemCardText } from "@/components/ItemCardText";
import { Loader } from "@/components/Loader";
import MoviePoster from "@/components/posters/MoviePoster.tv";
import SeriesPoster from "@/components/posters/SeriesPoster.tv";
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
import { TVItemCardText } from "@/components/tv/TVItemCardText";
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
import { useScaledTVTypography } from "@/constants/TVTypography";
import useRouter from "@/hooks/useAppRouter";
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
@@ -44,64 +46,6 @@ const ACTOR_IMAGE_SIZE = 250;
const ITEM_GAP = 16;
const SCALE_PADDING = 20;
// Focusable poster wrapper component for TV
const TVFocusablePoster: React.FC<{
children: React.ReactNode;
onPress: () => void;
onLongPress?: () => void;
hasTVPreferredFocus?: boolean;
onFocus?: () => void;
onBlur?: () => void;
}> = ({
children,
onPress,
onLongPress,
hasTVPreferredFocus,
onFocus,
onBlur,
}) => {
const [focused, setFocused] = useState(false);
const scale = useRef(new Animated.Value(1)).current;
const animateTo = (value: number) =>
Animated.timing(scale, {
toValue: value,
duration: 150,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}).start();
return (
<Pressable
onPress={onPress}
onLongPress={onLongPress}
onFocus={() => {
setFocused(true);
animateTo(1.05);
onFocus?.();
}}
onBlur={() => {
setFocused(false);
animateTo(1);
onBlur?.();
}}
hasTVPreferredFocus={hasTVPreferredFocus}
>
<Animated.View
style={{
transform: [{ scale }],
shadowColor: "#ffffff",
shadowOffset: { width: 0, height: 0 },
shadowOpacity: focused ? 0.6 : 0,
shadowRadius: focused ? 20 : 0,
}}
>
{children}
</Animated.View>
</Pressable>
);
};
interface TVActorPageProps {
personId: string;
}
@@ -114,6 +58,7 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
const segments = useSegments();
const from = (segments as string[])[2] || "(home)";
const posterSizes = useScaledTVPosterSizes();
const typography = useScaledTVTypography();
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -294,29 +239,48 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
[],
);
// Render filmography item
const renderFilmographyItem = useCallback(
(
{ item: filmItem, index }: { item: BaseItemDto; index: number },
isFirstSection: boolean,
) => (
// Render movie filmography item
const renderMovieItem = useCallback(
({ item: filmItem, index }: { item: BaseItemDto; index: number }) => (
<View style={{ marginRight: ITEM_GAP }}>
<TVFocusablePoster
onPress={() => handleItemPress(filmItem)}
onLongPress={() => showItemActions(filmItem)}
onFocus={() => setFocusedItem(filmItem)}
hasTVPreferredFocus={isFirstSection && index === 0}
hasTVPreferredFocus={index === 0}
>
<View>
<MoviePoster item={filmItem} />
<View style={{ width: posterSizes.poster, marginTop: 8 }}>
<ItemCardText item={filmItem} />
<View style={{ width: posterSizes.poster }}>
<TVItemCardText item={filmItem} />
</View>
</View>
</TVFocusablePoster>
</View>
),
[handleItemPress, showItemActions],
[handleItemPress, showItemActions, posterSizes.poster],
);
// Render series filmography item
const renderSeriesItem = useCallback(
({ item: filmItem, index }: { item: BaseItemDto; index: number }) => (
<View style={{ marginRight: ITEM_GAP }}>
<TVFocusablePoster
onPress={() => handleItemPress(filmItem)}
onLongPress={() => showItemActions(filmItem)}
onFocus={() => setFocusedItem(filmItem)}
hasTVPreferredFocus={movies.length === 0 && index === 0}
>
<View>
<SeriesPoster item={filmItem} />
<View style={{ width: posterSizes.poster }}>
<TVItemCardText item={filmItem} />
</View>
</View>
</TVFocusablePoster>
</View>
),
[handleItemPress, showItemActions, posterSizes.poster, movies.length],
);
if (isLoadingActor) {
@@ -386,28 +350,16 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
<View style={{ flex: 1, backgroundColor: "#1a1a1a" }} />
)}
</Animated.View>
{/* Gradient overlays for readability */}
{/* Gradient overlay for readability */}
<LinearGradient
colors={["transparent", "rgba(0,0,0,0.7)", "rgba(0,0,0,0.95)"]}
locations={[0, 0.5, 1]}
colors={["rgba(0,0,0,0.3)", "rgba(0,0,0,0.7)", "rgba(0,0,0,0.95)"]}
locations={[0, 0.4, 1]}
style={{
position: "absolute",
left: 0,
right: 0,
bottom: 0,
height: "70%",
}}
/>
<LinearGradient
colors={["rgba(0,0,0,0.8)", "transparent"]}
start={{ x: 0, y: 0 }}
end={{ x: 0.6, y: 0 }}
style={{
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: "60%",
height: "100%",
}}
/>
</View>
@@ -471,7 +423,7 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
{/* Actor name */}
<Text
style={{
fontSize: 42,
fontSize: typography.title,
fontWeight: "bold",
color: "#FFFFFF",
marginBottom: 8,
@@ -485,7 +437,7 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
{item.ProductionYear && (
<Text
style={{
fontSize: 18,
fontSize: typography.callout,
color: "#9CA3AF",
marginBottom: 16,
}}
@@ -498,9 +450,9 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
{item.Overview && (
<Text
style={{
fontSize: 18,
fontSize: typography.body,
color: "#D1D5DB",
lineHeight: 28,
lineHeight: typography.body * 1.4,
maxWidth: SCREEN_WIDTH * 0.45,
}}
numberOfLines={4}
@@ -529,7 +481,7 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
<View style={{ marginBottom: 32 }}>
<Text
style={{
fontSize: 22,
fontSize: typography.heading,
fontWeight: "600",
color: "#FFFFFF",
marginBottom: 16,
@@ -542,7 +494,7 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
horizontal
data={movies}
keyExtractor={(filmItem) => filmItem.Id!}
renderItem={(props) => renderFilmographyItem(props, true)}
renderItem={renderMovieItem}
showsHorizontalScrollIndicator={false}
initialNumToRender={6}
maxToRenderPerBatch={4}
@@ -575,7 +527,7 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
<View>
<Text
style={{
fontSize: 22,
fontSize: typography.heading,
fontWeight: "600",
color: "#FFFFFF",
marginBottom: 16,
@@ -588,9 +540,7 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
horizontal
data={series}
keyExtractor={(filmItem) => filmItem.Id!}
renderItem={(props) =>
renderFilmographyItem(props, movies.length === 0)
}
renderItem={renderSeriesItem}
showsHorizontalScrollIndicator={false}
initialNumToRender={6}
maxToRenderPerBatch={4}
@@ -615,7 +565,7 @@ export const TVActorPage: React.FC<TVActorPageProps> = ({ personId }) => {
<Text
style={{
color: "#737373",
fontSize: 16,
fontSize: typography.callout,
marginLeft: SCALE_PADDING,
}}
>