mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-03 11:02:52 +01:00
fix(tv): poster design and other stuff
This commit is contained in:
@@ -16,17 +16,15 @@ import {
|
||||
} from "react-native";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { getItemNavigation } from "@/components/common/TouchableItemRouter";
|
||||
import MoviePoster from "@/components/posters/MoviePoster.tv";
|
||||
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
|
||||
import { TVPosterCard } from "@/components/tv/TVPosterCard";
|
||||
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
||||
import { useScaledTVSizes } from "@/constants/TVSizes";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
||||
import ContinueWatchingPoster from "../ContinueWatchingPoster.tv";
|
||||
import SeriesPoster from "../posters/SeriesPoster.tv";
|
||||
|
||||
const ITEM_GAP = 24;
|
||||
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
||||
const SCALE_PADDING = 20;
|
||||
|
||||
@@ -47,76 +45,6 @@ interface Props extends ViewProps {
|
||||
}
|
||||
|
||||
type Typography = ReturnType<typeof useScaledTVTypography>;
|
||||
|
||||
// TV-specific ItemCardText with appropriately sized fonts
|
||||
const TVItemCardText: React.FC<{
|
||||
item: BaseItemDto;
|
||||
typography: Typography;
|
||||
width?: number;
|
||||
}> = ({ item, typography, width }) => {
|
||||
const renderSubtitle = () => {
|
||||
if (item.Type === "Episode") {
|
||||
return (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
fontSize: typography.callout,
|
||||
color: "#9CA3AF",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{`S${item.ParentIndexNumber?.toString()}:E${item.IndexNumber?.toString()}`}
|
||||
{" - "}
|
||||
{item.SeriesName}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (item.Type === "Program") {
|
||||
// For Live TV programs, show channel name
|
||||
const channelName = item.ChannelName;
|
||||
return channelName ? (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
fontSize: typography.callout,
|
||||
color: "#9CA3AF",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{channelName}
|
||||
</Text>
|
||||
) : null;
|
||||
}
|
||||
|
||||
// Default: show production year
|
||||
return item.ProductionYear ? (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
fontSize: typography.callout,
|
||||
color: "#9CA3AF",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{item.ProductionYear}
|
||||
</Text>
|
||||
) : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ marginTop: 12, flexDirection: "column", width }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{ fontSize: typography.body, color: "#FFFFFF" }}
|
||||
>
|
||||
{item.Name}
|
||||
</Text>
|
||||
{renderSubtitle()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
type PosterSizes = ReturnType<typeof useScaledTVPosterSizes>;
|
||||
|
||||
// TV-specific "See All" card for end of lists
|
||||
@@ -139,7 +67,7 @@ const TVSeeAllCard: React.FC<{
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const width =
|
||||
orientation === "horizontal" ? posterSizes.landscape : posterSizes.poster;
|
||||
orientation === "horizontal" ? posterSizes.episode : posterSizes.poster;
|
||||
const aspectRatio = orientation === "horizontal" ? 16 / 9 : 10 / 15;
|
||||
|
||||
return (
|
||||
@@ -200,6 +128,8 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
}) => {
|
||||
const typography = useScaledTVTypography();
|
||||
const posterSizes = useScaledTVPosterSizes();
|
||||
const sizes = useScaledTVSizes();
|
||||
const ITEM_GAP = sizes.gaps.item;
|
||||
const effectivePageSize = Math.max(1, pageSize);
|
||||
const hasCalledOnLoaded = useRef(false);
|
||||
const router = useRouter();
|
||||
@@ -279,7 +209,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
}, [data]);
|
||||
|
||||
const itemWidth =
|
||||
orientation === "horizontal" ? posterSizes.landscape : posterSizes.poster;
|
||||
orientation === "horizontal" ? posterSizes.episode : posterSizes.poster;
|
||||
|
||||
const handleItemPress = useCallback(
|
||||
(item: BaseItemDto) => {
|
||||
@@ -310,70 +240,17 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: BaseItemDto; index: number }) => {
|
||||
const isFirstItem = isFirstSection && index === 0;
|
||||
const isHorizontal = orientation === "horizontal";
|
||||
|
||||
const renderPoster = () => {
|
||||
if (item.Type === "Episode" && isHorizontal) {
|
||||
return <ContinueWatchingPoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Episode" && !isHorizontal) {
|
||||
return <SeriesPoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Movie" && isHorizontal) {
|
||||
return <ContinueWatchingPoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Movie" && !isHorizontal) {
|
||||
return <MoviePoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Series" && !isHorizontal) {
|
||||
return <SeriesPoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Series" && isHorizontal) {
|
||||
return <ContinueWatchingPoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Program") {
|
||||
return <ContinueWatchingPoster item={item} />;
|
||||
}
|
||||
if (item.Type === "BoxSet" && !isHorizontal) {
|
||||
return <MoviePoster item={item} />;
|
||||
}
|
||||
if (item.Type === "BoxSet" && isHorizontal) {
|
||||
return <ContinueWatchingPoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Playlist" && !isHorizontal) {
|
||||
return <MoviePoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Playlist" && isHorizontal) {
|
||||
return <ContinueWatchingPoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Video" && !isHorizontal) {
|
||||
return <MoviePoster item={item} />;
|
||||
}
|
||||
if (item.Type === "Video" && isHorizontal) {
|
||||
return <ContinueWatchingPoster item={item} />;
|
||||
}
|
||||
// Default fallback
|
||||
return isHorizontal ? (
|
||||
<ContinueWatchingPoster item={item} />
|
||||
) : (
|
||||
<MoviePoster item={item} />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ marginRight: ITEM_GAP, width: itemWidth }}>
|
||||
<TVFocusablePoster
|
||||
<View style={{ marginRight: ITEM_GAP }}>
|
||||
<TVPosterCard
|
||||
item={item}
|
||||
orientation={orientation}
|
||||
onPress={() => handleItemPress(item)}
|
||||
onLongPress={() => showItemActions(item)}
|
||||
hasTVPreferredFocus={isFirstItem}
|
||||
onFocus={() => handleItemFocus(item)}
|
||||
onBlur={handleItemBlur}
|
||||
>
|
||||
{renderPoster()}
|
||||
</TVFocusablePoster>
|
||||
<TVItemCardText
|
||||
item={item}
|
||||
typography={typography}
|
||||
width={itemWidth}
|
||||
/>
|
||||
</View>
|
||||
@@ -387,7 +264,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
||||
showItemActions,
|
||||
handleItemFocus,
|
||||
handleItemBlur,
|
||||
typography,
|
||||
ITEM_GAP,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ import { FlatList, View, type ViewProps } from "react-native";
|
||||
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { getItemNavigation } from "@/components/common/TouchableItemRouter";
|
||||
import MoviePoster from "@/components/posters/MoviePoster.tv";
|
||||
import SeriesPoster from "@/components/posters/SeriesPoster.tv";
|
||||
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
|
||||
import { TVPosterCard } from "@/components/tv/TVPosterCard";
|
||||
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
||||
import { useScaledTVSizes } from "@/constants/TVSizes";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||
@@ -23,36 +22,8 @@ import { useSettings } from "@/utils/atoms/settings";
|
||||
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
||||
import type { StreamystatsWatchlist } from "@/utils/streamystats/types";
|
||||
|
||||
const ITEM_GAP = 16;
|
||||
const SCALE_PADDING = 20;
|
||||
|
||||
type Typography = ReturnType<typeof useScaledTVTypography>;
|
||||
|
||||
const TVItemCardText: React.FC<{
|
||||
item: BaseItemDto;
|
||||
typography: Typography;
|
||||
}> = ({ item, typography }) => {
|
||||
return (
|
||||
<View style={{ marginTop: 12, flexDirection: "column" }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{ fontSize: typography.callout, color: "#FFFFFF" }}
|
||||
>
|
||||
{item.Name}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: typography.callout,
|
||||
color: "#9CA3AF",
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
{item.ProductionYear}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface WatchlistSectionProps extends ViewProps {
|
||||
watchlist: StreamystatsWatchlist;
|
||||
jellyfinServerId: string;
|
||||
@@ -67,6 +38,8 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
|
||||
}) => {
|
||||
const typography = useScaledTVTypography();
|
||||
const posterSizes = useScaledTVPosterSizes();
|
||||
const sizes = useScaledTVSizes();
|
||||
const ITEM_GAP = sizes.gaps.item;
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
const { settings } = useSettings();
|
||||
@@ -135,27 +108,31 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
|
||||
offset: (posterSizes.poster + ITEM_GAP) * index,
|
||||
index,
|
||||
}),
|
||||
[],
|
||||
[posterSizes.poster, ITEM_GAP],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: BaseItemDto }) => {
|
||||
return (
|
||||
<View style={{ marginRight: ITEM_GAP, width: posterSizes.poster }}>
|
||||
<TVFocusablePoster
|
||||
<View style={{ marginRight: ITEM_GAP }}>
|
||||
<TVPosterCard
|
||||
item={item}
|
||||
orientation='vertical'
|
||||
onPress={() => handleItemPress(item)}
|
||||
onLongPress={() => showItemActions(item)}
|
||||
onFocus={() => onItemFocus?.(item)}
|
||||
hasTVPreferredFocus={false}
|
||||
>
|
||||
{item.Type === "Movie" && <MoviePoster item={item} />}
|
||||
{item.Type === "Series" && <SeriesPoster item={item} />}
|
||||
</TVFocusablePoster>
|
||||
<TVItemCardText item={item} typography={typography} />
|
||||
width={posterSizes.poster}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[handleItemPress, showItemActions, onItemFocus, typography],
|
||||
[
|
||||
ITEM_GAP,
|
||||
posterSizes.poster,
|
||||
handleItemPress,
|
||||
showItemActions,
|
||||
onItemFocus,
|
||||
],
|
||||
);
|
||||
|
||||
if (!isLoading && (!items || items.length === 0)) return null;
|
||||
@@ -230,6 +207,8 @@ export const StreamystatsPromotedWatchlists: React.FC<
|
||||
StreamystatsPromotedWatchlistsProps
|
||||
> = ({ enabled = true, onItemFocus, ...props }) => {
|
||||
const posterSizes = useScaledTVPosterSizes();
|
||||
const sizes = useScaledTVSizes();
|
||||
const ITEM_GAP = sizes.gaps.item;
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
const { settings } = useSettings();
|
||||
|
||||
@@ -11,10 +11,8 @@ import { FlatList, View, type ViewProps } from "react-native";
|
||||
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { getItemNavigation } from "@/components/common/TouchableItemRouter";
|
||||
import MoviePoster from "@/components/posters/MoviePoster.tv";
|
||||
import SeriesPoster from "@/components/posters/SeriesPoster.tv";
|
||||
import { TVFocusablePoster } from "@/components/tv/TVFocusablePoster";
|
||||
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
||||
import { TVPosterCard } from "@/components/tv/TVPosterCard";
|
||||
import { useScaledTVSizes } from "@/constants/TVSizes";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||
@@ -23,11 +21,6 @@ import { useSettings } from "@/utils/atoms/settings";
|
||||
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
||||
import type { StreamystatsRecommendationsIdsResponse } from "@/utils/streamystats/types";
|
||||
|
||||
const ITEM_GAP = 16;
|
||||
const SCALE_PADDING = 20;
|
||||
|
||||
type Typography = ReturnType<typeof useScaledTVTypography>;
|
||||
|
||||
interface Props extends ViewProps {
|
||||
title: string;
|
||||
type: "Movie" | "Series";
|
||||
@@ -36,31 +29,6 @@ interface Props extends ViewProps {
|
||||
onItemFocus?: (item: BaseItemDto) => void;
|
||||
}
|
||||
|
||||
const TVItemCardText: React.FC<{
|
||||
item: BaseItemDto;
|
||||
typography: Typography;
|
||||
}> = ({ item, typography }) => {
|
||||
return (
|
||||
<View style={{ marginTop: 12, flexDirection: "column" }}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{ fontSize: typography.callout, color: "#FFFFFF" }}
|
||||
>
|
||||
{item.Name}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: typography.callout,
|
||||
color: "#9CA3AF",
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
{item.ProductionYear}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
title,
|
||||
type,
|
||||
@@ -70,7 +38,7 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
...props
|
||||
}) => {
|
||||
const typography = useScaledTVTypography();
|
||||
const posterSizes = useScaledTVPosterSizes();
|
||||
const sizes = useScaledTVSizes();
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
const { settings } = useSettings();
|
||||
@@ -192,31 +160,29 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
|
||||
const getItemLayout = useCallback(
|
||||
(_data: ArrayLike<BaseItemDto> | null | undefined, index: number) => ({
|
||||
length: posterSizes.poster + ITEM_GAP,
|
||||
offset: (posterSizes.poster + ITEM_GAP) * index,
|
||||
length: sizes.posters.poster + sizes.gaps.item,
|
||||
offset: (sizes.posters.poster + sizes.gaps.item) * index,
|
||||
index,
|
||||
}),
|
||||
[],
|
||||
[sizes],
|
||||
);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: BaseItemDto }) => {
|
||||
return (
|
||||
<View style={{ marginRight: ITEM_GAP, width: posterSizes.poster }}>
|
||||
<TVFocusablePoster
|
||||
<View style={{ marginRight: sizes.gaps.item }}>
|
||||
<TVPosterCard
|
||||
item={item}
|
||||
orientation='vertical'
|
||||
onPress={() => handleItemPress(item)}
|
||||
onLongPress={() => showItemActions(item)}
|
||||
onFocus={() => onItemFocus?.(item)}
|
||||
hasTVPreferredFocus={false}
|
||||
>
|
||||
{item.Type === "Movie" && <MoviePoster item={item} />}
|
||||
{item.Type === "Series" && <SeriesPoster item={item} />}
|
||||
</TVFocusablePoster>
|
||||
<TVItemCardText item={item} typography={typography} />
|
||||
width={sizes.posters.poster}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[handleItemPress, showItemActions, onItemFocus, typography],
|
||||
[sizes, handleItemPress, showItemActions, onItemFocus],
|
||||
);
|
||||
|
||||
if (!streamyStatsEnabled) return null;
|
||||
@@ -231,7 +197,7 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
fontWeight: "700",
|
||||
color: "#FFFFFF",
|
||||
marginBottom: 20,
|
||||
marginLeft: SCALE_PADDING,
|
||||
marginLeft: sizes.padding.scale,
|
||||
letterSpacing: 0.5,
|
||||
}}
|
||||
>
|
||||
@@ -242,17 +208,17 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: ITEM_GAP,
|
||||
paddingHorizontal: SCALE_PADDING,
|
||||
paddingVertical: SCALE_PADDING,
|
||||
gap: sizes.gaps.item,
|
||||
paddingHorizontal: sizes.padding.scale,
|
||||
paddingVertical: sizes.padding.scale,
|
||||
}}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<View key={i} style={{ width: posterSizes.poster }}>
|
||||
<View key={i} style={{ width: sizes.posters.poster }}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#262626",
|
||||
width: posterSizes.poster,
|
||||
width: sizes.posters.poster,
|
||||
aspectRatio: 10 / 15,
|
||||
borderRadius: 12,
|
||||
marginBottom: 8,
|
||||
@@ -275,8 +241,8 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
||||
getItemLayout={getItemLayout}
|
||||
style={{ overflow: "visible" }}
|
||||
contentContainerStyle={{
|
||||
paddingVertical: SCALE_PADDING,
|
||||
paddingHorizontal: SCALE_PADDING,
|
||||
paddingVertical: sizes.padding.scale,
|
||||
paddingHorizontal: sizes.padding.scale,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { ProgressBar } from "@/components/common/ProgressBar";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { getItemNavigation } from "@/components/common/TouchableItemRouter";
|
||||
import { useScaledTVPosterSizes } from "@/constants/TVPosterSizes";
|
||||
import { type ScaledTVSizes, useScaledTVSizes } from "@/constants/TVSizes";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import {
|
||||
@@ -36,9 +36,6 @@ import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"
|
||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||
|
||||
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
|
||||
const HERO_HEIGHT = SCREEN_HEIGHT * 0.62;
|
||||
const CARD_GAP = 24;
|
||||
const CARD_PADDING = 60;
|
||||
|
||||
interface TVHeroCarouselProps {
|
||||
items: BaseItemDto[];
|
||||
@@ -49,14 +46,14 @@ interface TVHeroCarouselProps {
|
||||
interface HeroCardProps {
|
||||
item: BaseItemDto;
|
||||
isFirst: boolean;
|
||||
cardWidth: number;
|
||||
sizes: ScaledTVSizes;
|
||||
onFocus: (item: BaseItemDto) => void;
|
||||
onPress: (item: BaseItemDto) => void;
|
||||
onLongPress?: (item: BaseItemDto) => void;
|
||||
}
|
||||
|
||||
const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
({ item, isFirst, cardWidth, onFocus, onPress, onLongPress }) => {
|
||||
({ item, isFirst, sizes, onFocus, onPress, onLongPress }) => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const scale = useRef(new Animated.Value(1)).current;
|
||||
@@ -87,8 +84,6 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
return null;
|
||||
}, [api, item]);
|
||||
|
||||
const progress = item.UserData?.PlayedPercentage || 0;
|
||||
|
||||
const animateTo = useCallback(
|
||||
(value: number) =>
|
||||
Animated.timing(scale, {
|
||||
@@ -102,9 +97,9 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setFocused(true);
|
||||
animateTo(1.1);
|
||||
animateTo(sizes.animation.focusScale);
|
||||
onFocus(item);
|
||||
}, [animateTo, onFocus, item]);
|
||||
}, [animateTo, onFocus, item, sizes.animation.focusScale]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocused(false);
|
||||
@@ -120,7 +115,8 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
}, [onLongPress, item]);
|
||||
|
||||
// Use glass poster for tvOS 26+
|
||||
if (useGlass) {
|
||||
if (useGlass && posterUrl) {
|
||||
const progress = item.UserData?.PlayedPercentage || 0;
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
@@ -128,17 +124,17 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={isFirst}
|
||||
style={{ marginRight: CARD_GAP }}
|
||||
style={{ marginRight: sizes.gaps.item }}
|
||||
>
|
||||
<GlassPosterView
|
||||
imageUrl={posterUrl}
|
||||
aspectRatio={16 / 9}
|
||||
cornerRadius={16}
|
||||
cornerRadius={24}
|
||||
progress={progress}
|
||||
showWatchedIndicator={false}
|
||||
isFocused={focused}
|
||||
width={cardWidth}
|
||||
style={{ width: cardWidth }}
|
||||
width={sizes.posters.episode}
|
||||
style={{ width: sizes.posters.episode }}
|
||||
/>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -152,13 +148,13 @@ const HeroCard: React.FC<HeroCardProps> = React.memo(
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={isFirst}
|
||||
style={{ marginRight: CARD_GAP }}
|
||||
style={{ marginRight: sizes.gaps.item }}
|
||||
>
|
||||
<Animated.View
|
||||
style={{
|
||||
width: cardWidth,
|
||||
width: sizes.posters.episode,
|
||||
aspectRatio: 16 / 9,
|
||||
borderRadius: 16,
|
||||
borderRadius: 24,
|
||||
overflow: "hidden",
|
||||
transform: [{ scale }],
|
||||
shadowColor: "#FFFFFF",
|
||||
@@ -206,7 +202,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
onItemLongPress,
|
||||
}) => {
|
||||
const typography = useScaledTVTypography();
|
||||
const posterSizes = useScaledTVPosterSizes();
|
||||
const sizes = useScaledTVSizes();
|
||||
const api = useAtomValue(apiAtom);
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
@@ -365,13 +361,13 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
<HeroCard
|
||||
item={item}
|
||||
isFirst={index === 0}
|
||||
cardWidth={posterSizes.heroCard}
|
||||
sizes={sizes}
|
||||
onFocus={handleCardFocus}
|
||||
onPress={handleCardPress}
|
||||
onLongPress={onItemLongPress}
|
||||
/>
|
||||
),
|
||||
[handleCardFocus, handleCardPress, onItemLongPress, posterSizes.heroCard],
|
||||
[handleCardFocus, handleCardPress, onItemLongPress, sizes],
|
||||
);
|
||||
|
||||
// Memoize keyExtractor
|
||||
@@ -379,8 +375,10 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
const heroHeight = SCREEN_HEIGHT * sizes.padding.heroHeight;
|
||||
|
||||
return (
|
||||
<View style={{ height: HERO_HEIGHT, width: "100%" }}>
|
||||
<View style={{ height: heroHeight, width: "100%" }}>
|
||||
{/* Backdrop layers with crossfade */}
|
||||
<View
|
||||
style={{
|
||||
@@ -469,8 +467,8 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: insets.left + CARD_PADDING,
|
||||
right: insets.right + CARD_PADDING,
|
||||
left: insets.left + sizes.padding.horizontal,
|
||||
right: insets.right + sizes.padding.horizontal,
|
||||
bottom: 40,
|
||||
}}
|
||||
>
|
||||
@@ -624,7 +622,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
keyExtractor={keyExtractor}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={{ overflow: "visible" }}
|
||||
contentContainerStyle={{ paddingVertical: 12 }}
|
||||
contentContainerStyle={{ paddingVertical: sizes.gaps.small }}
|
||||
renderItem={renderHeroCard}
|
||||
removeClippedSubviews={false}
|
||||
initialNumToRender={8}
|
||||
|
||||
Reference in New Issue
Block a user