fix(tv): poster design and other stuff

This commit is contained in:
Fredrik Burmester
2026-01-30 09:15:44 +01:00
parent 0cd74519d4
commit aed3a8f493
26 changed files with 758 additions and 1362 deletions

View File

@@ -21,7 +21,7 @@ export const TVActorCard = React.forwardRef<View, TVActorCardProps>(
({ person, apiBasePath, onPress, hasTVPreferredFocus }, ref) => {
const typography = useScaledTVTypography();
const { focused, handleFocus, handleBlur, animatedStyle } =
useTVFocusAnimation({ scaleAmount: 1.08 });
useTVFocusAnimation();
const imageUrl = person.Id
? `${apiBasePath}/Items/${person.Id}/Images/Primary?fillWidth=280&fillHeight=280&quality=90`

View File

@@ -3,6 +3,7 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, TVFocusGuideView, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useScaledTVSizes } from "@/constants/TVSizes";
import { useScaledTVTypography } from "@/constants/TVTypography";
import { TVActorCard } from "./TVActorCard";
@@ -25,6 +26,7 @@ export const TVCastSection: React.FC<TVCastSectionProps> = React.memo(
upwardFocusDestination,
}) => {
const typography = useScaledTVTypography();
const sizes = useScaledTVSizes();
const { t } = useTranslation();
if (cast.length === 0) {
@@ -57,7 +59,7 @@ export const TVCastSection: React.FC<TVCastSectionProps> = React.memo(
contentContainerStyle={{
paddingHorizontal: 80,
paddingVertical: 16,
gap: 28,
gap: sizes.gaps.item,
}}
>
{cast.map((person, index) => (

View File

@@ -0,0 +1,221 @@
import React, { useCallback } from "react";
import { FlatList, ScrollView, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useScaledTVSizes } from "@/constants/TVSizes";
import { useScaledTVTypography } from "@/constants/TVTypography";
interface TVHorizontalListProps<T> {
/** Data items to render */
data: T[];
/** Unique key extractor */
keyExtractor: (item: T, index: number) => string;
/** Render function for each item */
renderItem: (info: { item: T; index: number }) => React.ReactElement | null;
/** Optional section title */
title?: string;
/** Text to show when data array is empty */
emptyText?: string;
/** Whether to use FlatList (for large/infinite lists) or ScrollView (for small lists) */
useFlatList?: boolean;
/** Called when end is reached (only for FlatList) */
onEndReached?: () => void;
/** Ref for the scroll view */
scrollViewRef?: React.RefObject<ScrollView | FlatList<T> | null>;
/** Footer component (only for FlatList) */
ListFooterComponent?: React.ReactElement | null;
/** Whether this is the first section (for initial focus) */
isFirstSection?: boolean;
/** Loading state */
isLoading?: boolean;
/** Skeleton item count when loading */
skeletonCount?: number;
/** Skeleton render function */
renderSkeleton?: () => React.ReactElement;
/**
* Custom horizontal padding (overrides default sizes.padding.scale).
* Use this when the list needs to extend beyond its parent's padding.
* The list will use negative margin to extend beyond the parent,
* then add this padding inside to align content properly.
*/
horizontalPadding?: number;
}
/**
* TVHorizontalList - A unified horizontal list component for TV.
*
* Provides consistent spacing and layout for horizontal lists:
* - Uses `sizes.gaps.item` (24px default) for gap between items
* - Uses `sizes.padding.scale` (20px default) for padding to accommodate focus scale
* - Supports both ScrollView (small lists) and FlatList (large/infinite lists)
*/
export function TVHorizontalList<T>({
data,
keyExtractor,
renderItem,
title,
emptyText,
useFlatList = false,
onEndReached,
scrollViewRef,
ListFooterComponent,
isLoading = false,
skeletonCount = 5,
renderSkeleton,
horizontalPadding,
}: TVHorizontalListProps<T>) {
const sizes = useScaledTVSizes();
const typography = useScaledTVTypography();
// Use custom horizontal padding if provided, otherwise use default scale padding
const effectiveHorizontalPadding = horizontalPadding ?? sizes.padding.scale;
// Apply negative margin when using custom padding to extend beyond parent
const marginHorizontal = horizontalPadding ? -horizontalPadding : 0;
// Wrap renderItem to add consistent gap
const renderItemWithGap = useCallback(
({ item, index }: { item: T; index: number }) => {
const isLast = index === data.length - 1;
return (
<View style={{ marginRight: isLast ? 0 : sizes.gaps.item }}>
{renderItem({ item, index })}
</View>
);
},
[data.length, renderItem, sizes.gaps.item],
);
// Empty state
if (!isLoading && data.length === 0 && emptyText) {
return (
<View style={{ overflow: "visible" }}>
{title && (
<Text
style={{
fontSize: typography.heading,
fontWeight: "700",
color: "#FFFFFF",
marginBottom: 20,
marginLeft: sizes.padding.scale,
letterSpacing: 0.5,
}}
>
{title}
</Text>
)}
<Text
style={{
color: "#737373",
fontSize: typography.callout,
marginLeft: sizes.padding.scale,
}}
>
{emptyText}
</Text>
</View>
);
}
// Loading state
if (isLoading && renderSkeleton) {
return (
<View style={{ overflow: "visible" }}>
{title && (
<Text
style={{
fontSize: typography.heading,
fontWeight: "700",
color: "#FFFFFF",
marginBottom: 20,
marginLeft: sizes.padding.scale,
letterSpacing: 0.5,
}}
>
{title}
</Text>
)}
<View
style={{
flexDirection: "row",
gap: sizes.gaps.item,
paddingHorizontal: sizes.padding.scale,
paddingVertical: sizes.padding.scale,
}}
>
{Array.from({ length: skeletonCount }).map((_, i) => (
<View key={i}>{renderSkeleton()}</View>
))}
</View>
</View>
);
}
const contentContainerStyle = {
paddingHorizontal: effectiveHorizontalPadding,
paddingVertical: sizes.padding.scale,
};
const listStyle = {
overflow: "visible" as const,
marginHorizontal,
};
return (
<View style={{ overflow: "visible" }}>
{title && (
<Text
style={{
fontSize: typography.heading,
fontWeight: "700",
color: "#FFFFFF",
marginBottom: 20,
marginLeft: sizes.padding.scale,
letterSpacing: 0.5,
}}
>
{title}
</Text>
)}
{useFlatList ? (
<FlatList
ref={scrollViewRef as React.RefObject<FlatList<T>>}
horizontal
data={data}
keyExtractor={keyExtractor}
renderItem={renderItemWithGap}
showsHorizontalScrollIndicator={false}
removeClippedSubviews={false}
style={listStyle}
contentContainerStyle={contentContainerStyle}
onEndReached={onEndReached}
onEndReachedThreshold={0.5}
initialNumToRender={5}
maxToRenderPerBatch={3}
windowSize={5}
maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
ListFooterComponent={ListFooterComponent}
/>
) : (
<ScrollView
ref={scrollViewRef as React.RefObject<ScrollView>}
horizontal
showsHorizontalScrollIndicator={false}
style={listStyle}
contentContainerStyle={contentContainerStyle}
>
{data.map((item, index) => (
<View
key={keyExtractor(item, index)}
style={{
marginRight: index === data.length - 1 ? 0 : sizes.gaps.item,
}}
>
{renderItem({ item, index })}
</View>
))}
{ListFooterComponent}
</ScrollView>
)}
</View>
);
}

View File

@@ -3,6 +3,7 @@ import React from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, View } from "react-native";
import { Text } from "@/components/common/Text";
import { useScaledTVSizes } from "@/constants/TVSizes";
import { useScaledTVTypography } from "@/constants/TVTypography";
import { TVSeriesSeasonCard } from "./TVSeriesSeasonCard";
@@ -17,6 +18,7 @@ export interface TVSeriesNavigationProps {
export const TVSeriesNavigation: React.FC<TVSeriesNavigationProps> = React.memo(
({ item, seriesImageUrl, seasonImageUrl, onSeriesPress, onSeasonPress }) => {
const typography = useScaledTVTypography();
const sizes = useScaledTVSizes();
const { t } = useTranslation();
// Only show for episodes with a series
@@ -25,13 +27,14 @@ export const TVSeriesNavigation: React.FC<TVSeriesNavigationProps> = React.memo(
}
return (
<View style={{ marginBottom: 32 }}>
<View style={{ marginBottom: sizes.gaps.section }}>
<Text
style={{
fontSize: typography.heading,
fontWeight: "600",
fontWeight: "700",
color: "#FFFFFF",
marginBottom: 24,
marginBottom: 20,
letterSpacing: 0.5,
}}
>
{t("item_card.from_this_series") || "From this Series"}
@@ -39,11 +42,14 @@ export const TVSeriesNavigation: React.FC<TVSeriesNavigationProps> = React.memo(
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={{ marginHorizontal: -80, overflow: "visible" }}
style={{
marginHorizontal: -sizes.padding.horizontal,
overflow: "visible",
}}
contentContainerStyle={{
paddingHorizontal: 80,
paddingVertical: 12,
gap: 24,
paddingHorizontal: sizes.padding.horizontal,
paddingVertical: sizes.padding.scale,
gap: sizes.gaps.item,
}}
>
{/* Series card */}

View File

@@ -57,7 +57,7 @@ export const TVSettingsToggle: React.FC<TVSettingsToggleProps> = ({
width: 56,
height: 32,
borderRadius: 16,
backgroundColor: value ? "#34C759" : "#4B5563",
backgroundColor: value ? "#FFFFFF" : "#4B5563",
justifyContent: "center",
paddingHorizontal: 2,
}}
@@ -67,7 +67,7 @@ export const TVSettingsToggle: React.FC<TVSettingsToggleProps> = ({
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: "#FFFFFF",
backgroundColor: value ? "#000000" : "#FFFFFF",
alignSelf: value ? "flex-end" : "flex-start",
}}
/>