mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-29 03:46:29 +01:00
Compare commits
3 Commits
feat/tv-in
...
feat/tv-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bab11addee | ||
|
|
8ee1197186 | ||
|
|
8c21054d33 |
@@ -20,7 +20,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Animated,
|
Animated,
|
||||||
|
Dimensions,
|
||||||
Easing,
|
Easing,
|
||||||
|
PixelRatio,
|
||||||
|
Platform,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
@@ -40,11 +43,12 @@ import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
|||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||||
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
|
|
||||||
const HORIZONTAL_PADDING = 60;
|
const HORIZONTAL_PADDING = scaleSize(60);
|
||||||
const TOP_PADDING = 100;
|
const TOP_PADDING = scaleSize(100);
|
||||||
// Generous gap between sections for Apple TV+ aesthetic
|
// Generous gap between sections for Apple TV+ aesthetic
|
||||||
const SECTION_GAP = 24;
|
const SECTION_GAP = scaleSize(10);
|
||||||
|
|
||||||
type InfiniteScrollingCollectionListSection = {
|
type InfiniteScrollingCollectionListSection = {
|
||||||
type: "InfiniteScrollingCollectionList";
|
type: "InfiniteScrollingCollectionList";
|
||||||
@@ -79,6 +83,22 @@ export const Home = () => {
|
|||||||
const _invalidateCache = useInvalidatePlaybackProgressCache();
|
const _invalidateCache = useInvalidatePlaybackProgressCache();
|
||||||
const { showItemActions } = useTVItemActionModal();
|
const { showItemActions } = useTVItemActionModal();
|
||||||
|
|
||||||
|
// Log TV viewport dimensions for DPI scaling debug
|
||||||
|
useEffect(() => {
|
||||||
|
const w = Dimensions.get("window");
|
||||||
|
const s = Dimensions.get("screen");
|
||||||
|
console.log("========== TV DIMENSIONS ==========");
|
||||||
|
console.log("Platform.OS:", Platform.OS, "isTV:", Platform.isTV);
|
||||||
|
console.log("Window:", w.width, "x", w.height);
|
||||||
|
console.log("Screen:", s.width, "x", s.height);
|
||||||
|
console.log("PixelRatio:", PixelRatio.get());
|
||||||
|
console.log(
|
||||||
|
"scaleSize(210):",
|
||||||
|
210 * Math.min(w.width / 1920, w.height / 1080),
|
||||||
|
);
|
||||||
|
console.log("====================================");
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Dynamic backdrop state with debounce
|
// Dynamic backdrop state with debounce
|
||||||
const [focusedItem, setFocusedItem] = useState<BaseItemDto | null>(null);
|
const [focusedItem, setFocusedItem] = useState<BaseItemDto | null>(null);
|
||||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ import { useScaledTVTypography } from "@/constants/TVTypography";
|
|||||||
import useRouter from "@/hooks/useAppRouter";
|
import useRouter from "@/hooks/useAppRouter";
|
||||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||||
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
import { SortByOption, SortOrderOption } from "@/utils/atoms/filters";
|
||||||
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
|
|
||||||
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
// Extra padding to accommodate scale animation (1.05x) and glow shadow
|
||||||
const SCALE_PADDING = 20;
|
const SCALE_PADDING = scaleSize(20);
|
||||||
|
|
||||||
interface Props extends ViewProps {
|
interface Props extends ViewProps {
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@@ -81,7 +82,7 @@ const TVSeeAllCard: React.FC<{
|
|||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
borderRadius: 24,
|
borderRadius: scaleSize(24),
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
backgroundColor: "rgba(255, 255, 255, 0.08)",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -91,9 +92,9 @@ const TVSeeAllCard: React.FC<{
|
|||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name='arrow-forward'
|
name='arrow-forward'
|
||||||
size={32}
|
size={scaleSize(32)}
|
||||||
color='white'
|
color='white'
|
||||||
style={{ marginBottom: 8 }}
|
style={{ marginBottom: scaleSize(8) }}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -250,7 +251,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
fontSize: typography.heading,
|
fontSize: typography.heading,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
marginBottom: 20,
|
marginBottom: scaleSize(20),
|
||||||
marginLeft: sizes.padding.horizontal,
|
marginLeft: sizes.padding.horizontal,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
}}
|
}}
|
||||||
@@ -286,8 +287,8 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
width: itemWidth,
|
width: itemWidth,
|
||||||
aspectRatio: orientation === "horizontal" ? 16 / 9 : 10 / 15,
|
aspectRatio: orientation === "horizontal" ? 16 / 9 : 10 / 15,
|
||||||
borderRadius: 12,
|
borderRadius: scaleSize(12),
|
||||||
marginBottom: 8,
|
marginBottom: scaleSize(8),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
@@ -328,6 +329,9 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
windowSize={5}
|
windowSize={5}
|
||||||
removeClippedSubviews={false}
|
removeClippedSubviews={false}
|
||||||
maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
|
maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
|
||||||
|
ListHeaderComponent={
|
||||||
|
<View style={{ width: sizes.padding.horizontal }} />
|
||||||
|
}
|
||||||
style={{ overflow: "visible" }}
|
style={{ overflow: "visible" }}
|
||||||
contentInset={{
|
contentInset={{
|
||||||
left: sizes.padding.horizontal,
|
left: sizes.padding.horizontal,
|
||||||
@@ -342,6 +346,7 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
width: sizes.padding.horizontal,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isFetchingNextPage && (
|
{isFetchingNextPage && (
|
||||||
@@ -350,7 +355,10 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
|
|||||||
marginLeft: itemWidth / 2,
|
marginLeft: itemWidth / 2,
|
||||||
marginRight: ITEM_GAP,
|
marginRight: ITEM_GAP,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
height: orientation === "horizontal" ? 191 : 315,
|
height:
|
||||||
|
orientation === "horizontal"
|
||||||
|
? scaleSize(191)
|
||||||
|
: scaleSize(315),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ActivityIndicator size='small' color='white' />
|
<ActivityIndicator size='small' color='white' />
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ import useRouter from "@/hooks/useAppRouter";
|
|||||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
||||||
import type { StreamystatsWatchlist } from "@/utils/streamystats/types";
|
import type { StreamystatsWatchlist } from "@/utils/streamystats/types";
|
||||||
|
|
||||||
const SCALE_PADDING = 20;
|
const SCALE_PADDING = scaleSize(20);
|
||||||
|
|
||||||
interface WatchlistSectionProps extends ViewProps {
|
interface WatchlistSectionProps extends ViewProps {
|
||||||
watchlist: StreamystatsWatchlist;
|
watchlist: StreamystatsWatchlist;
|
||||||
@@ -168,8 +169,8 @@ const WatchlistSection: React.FC<WatchlistSectionProps> = ({
|
|||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
width: posterSizes.poster,
|
width: posterSizes.poster,
|
||||||
aspectRatio: 10 / 15,
|
aspectRatio: 10 / 15,
|
||||||
borderRadius: 12,
|
borderRadius: scaleSize(12),
|
||||||
marginBottom: 8,
|
marginBottom: scaleSize(8),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -286,12 +287,12 @@ export const StreamystatsPromotedWatchlists: React.FC<
|
|||||||
<View style={{ overflow: "visible" }} {...props}>
|
<View style={{ overflow: "visible" }} {...props}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
height: 16,
|
height: scaleSize(16),
|
||||||
width: 128,
|
width: scaleSize(128),
|
||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
borderRadius: 4,
|
borderRadius: scaleSize(4),
|
||||||
marginLeft: SCALE_PADDING,
|
marginLeft: SCALE_PADDING,
|
||||||
marginBottom: 16,
|
marginBottom: scaleSize(16),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
@@ -309,8 +310,8 @@ export const StreamystatsPromotedWatchlists: React.FC<
|
|||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
width: posterSizes.poster,
|
width: posterSizes.poster,
|
||||||
aspectRatio: 10 / 15,
|
aspectRatio: 10 / 15,
|
||||||
borderRadius: 12,
|
borderRadius: scaleSize(12),
|
||||||
marginBottom: 8,
|
marginBottom: scaleSize(8),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import useRouter from "@/hooks/useAppRouter";
|
|||||||
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
import { useTVItemActionModal } from "@/hooks/useTVItemActionModal";
|
||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
import { createStreamystatsApi } from "@/utils/streamystats/api";
|
||||||
import type { StreamystatsRecommendationsIdsResponse } from "@/utils/streamystats/types";
|
import type { StreamystatsRecommendationsIdsResponse } from "@/utils/streamystats/types";
|
||||||
|
|
||||||
@@ -220,8 +221,8 @@ export const StreamystatsRecommendations: React.FC<Props> = ({
|
|||||||
backgroundColor: "#262626",
|
backgroundColor: "#262626",
|
||||||
width: sizes.posters.poster,
|
width: sizes.posters.poster,
|
||||||
aspectRatio: 10 / 15,
|
aspectRatio: 10 / 15,
|
||||||
borderRadius: 12,
|
borderRadius: scaleSize(12),
|
||||||
marginBottom: 8,
|
marginBottom: scaleSize(8),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "@/modules/glass-poster";
|
} from "@/modules/glass-poster";
|
||||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||||
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||||
|
|
||||||
export interface TVPosterCardProps {
|
export interface TVPosterCardProps {
|
||||||
@@ -225,7 +226,13 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: scaleSize(8),
|
||||||
|
}}
|
||||||
|
>
|
||||||
{episodeLabel && (
|
{episodeLabel && (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -259,7 +266,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: "#9CA3AF",
|
color: "#9CA3AF",
|
||||||
marginTop: 4,
|
marginTop: scaleSize(4),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.ChannelName}
|
{item.ChannelName}
|
||||||
@@ -277,7 +284,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: "#9CA3AF",
|
color: "#9CA3AF",
|
||||||
marginTop: 4,
|
marginTop: scaleSize(4),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{artist}
|
{artist}
|
||||||
@@ -296,7 +303,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: "#9CA3AF",
|
color: "#9CA3AF",
|
||||||
marginTop: 4,
|
marginTop: scaleSize(4),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{artist}
|
{artist}
|
||||||
@@ -312,7 +319,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: "#9CA3AF",
|
color: "#9CA3AF",
|
||||||
marginTop: 4,
|
marginTop: scaleSize(4),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.ChildCount} tracks
|
{item.ChildCount} tracks
|
||||||
@@ -328,7 +335,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: "#9CA3AF",
|
color: "#9CA3AF",
|
||||||
marginTop: 4,
|
marginTop: scaleSize(4),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.ProductionYear}
|
{item.ProductionYear}
|
||||||
@@ -344,23 +351,23 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: 12,
|
top: scaleSize(12),
|
||||||
left: 12,
|
left: scaleSize(12),
|
||||||
backgroundColor: "#FFFFFF",
|
backgroundColor: "#FFFFFF",
|
||||||
borderRadius: 8,
|
borderRadius: scaleSize(8),
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: scaleSize(12),
|
||||||
paddingVertical: 8,
|
paddingVertical: scaleSize(8),
|
||||||
gap: 6,
|
gap: scaleSize(6),
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name='play' size={16} color='#000000' />
|
<Ionicons name='play' size={scaleSize(16)} color='#000000' />
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
color: "#000000",
|
color: "#000000",
|
||||||
fontSize: 14,
|
fontSize: scaleSize(14),
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -382,7 +389,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name='play-circle' size={56} color='white' />
|
<Ionicons name='play-circle' size={scaleSize(56)} color='white' />
|
||||||
</View>
|
</View>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -395,9 +402,9 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
width,
|
width,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
borderRadius: 24,
|
borderRadius: scaleSize(24),
|
||||||
backgroundColor: "#1a1a1a",
|
backgroundColor: "#1a1a1a",
|
||||||
borderWidth: 2,
|
borderWidth: scaleSize(2),
|
||||||
borderColor: focused ? "#FFFFFF" : "transparent",
|
borderColor: focused ? "#FFFFFF" : "transparent",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -411,7 +418,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
<GlassPosterView
|
<GlassPosterView
|
||||||
imageUrl={imageUrl}
|
imageUrl={imageUrl}
|
||||||
aspectRatio={aspectRatio}
|
aspectRatio={aspectRatio}
|
||||||
cornerRadius={24}
|
cornerRadius={scaleSize(24)}
|
||||||
progress={progress}
|
progress={progress}
|
||||||
showWatchedIndicator={isWatched}
|
showWatchedIndicator={isWatched}
|
||||||
isFocused={focused}
|
isFocused={focused}
|
||||||
@@ -431,10 +438,10 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
width,
|
width,
|
||||||
aspectRatio,
|
aspectRatio,
|
||||||
borderRadius: 24,
|
borderRadius: scaleSize(4),
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
backgroundColor: "#1a1a1a",
|
backgroundColor: "#1a1a1a",
|
||||||
borderWidth: 2,
|
borderWidth: scaleSize(2),
|
||||||
borderColor: focused ? "#FFFFFF" : "transparent",
|
borderColor: focused ? "#FFFFFF" : "transparent",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -470,7 +477,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: typography.callout,
|
fontSize: typography.callout,
|
||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
marginTop: 4,
|
marginTop: scaleSize(4),
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -498,8 +505,13 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
// Default: show name
|
// Default: show name
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
numberOfLines={1}
|
numberOfLines={3}
|
||||||
style={{ fontSize: typography.body, color: "#FFFFFF" }}
|
style={{
|
||||||
|
fontSize: typography.callout,
|
||||||
|
color: "#FFFFFF",
|
||||||
|
marginTop: scaleSize(4),
|
||||||
|
fontWeight: "500",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.Name}
|
{item.Name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -551,7 +563,7 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
shadowColor: useGlass ? undefined : shadowColor,
|
shadowColor: useGlass ? undefined : shadowColor,
|
||||||
shadowOffset: useGlass ? undefined : { width: 0, height: 0 },
|
shadowOffset: useGlass ? undefined : { width: 0, height: 0 },
|
||||||
shadowOpacity: useGlass ? undefined : focused ? 0.3 : 0,
|
shadowOpacity: useGlass ? undefined : focused ? 0.3 : 0,
|
||||||
shadowRadius: useGlass ? undefined : focused ? 12 : 0,
|
shadowRadius: useGlass ? undefined : focused ? scaleSize(12) : 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{renderPosterImage()}
|
{renderPosterImage()}
|
||||||
@@ -560,7 +572,9 @@ export const TVPosterCard: React.FC<TVPosterCardProps> = ({
|
|||||||
|
|
||||||
{/* Text below poster */}
|
{/* Text below poster */}
|
||||||
{showText && (
|
{showText && (
|
||||||
<View style={{ marginTop: 12, paddingHorizontal: 4 }}>
|
<View
|
||||||
|
style={{ marginTop: scaleSize(12), paddingHorizontal: scaleSize(4) }}
|
||||||
|
>
|
||||||
{item.Type === "Episode" ? (
|
{item.Type === "Episode" ? (
|
||||||
<>
|
<>
|
||||||
{renderSubtitle()}
|
{renderSubtitle()}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { TVTypographyScale, useSettings } from "@/utils/atoms/settings";
|
import { TVTypographyScale, useSettings } from "@/utils/atoms/settings";
|
||||||
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TV Layout Sizes
|
* TV Layout Sizes
|
||||||
*
|
*
|
||||||
* Unified constants for TV interface layout including posters, gaps, and padding.
|
* Unified constants for TV interface layout including posters, gaps, and padding.
|
||||||
* All values scale based on the user's tvTypographyScale setting.
|
* Base values are designed for 1920x1080 and scaled to the actual viewport via
|
||||||
|
* scaleSize(), then further adjusted by the user's tvTypographyScale setting.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -48,7 +50,7 @@ export const TVGaps = {
|
|||||||
*/
|
*/
|
||||||
export const TVPadding = {
|
export const TVPadding = {
|
||||||
/** Horizontal padding from screen edges */
|
/** Horizontal padding from screen edges */
|
||||||
horizontal: 60,
|
horizontal: 90,
|
||||||
|
|
||||||
/** Padding to accommodate scale animations (1.05x) */
|
/** Padding to accommodate scale animations (1.05x) */
|
||||||
scale: 20,
|
scale: 20,
|
||||||
@@ -129,20 +131,20 @@ export const useScaledTVSizes = (): ScaledTVSizes => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
posters: {
|
posters: {
|
||||||
poster: Math.round(TVPosterSizes.poster * scale),
|
poster: Math.round(scaleSize(TVPosterSizes.poster) * scale),
|
||||||
landscape: Math.round(TVPosterSizes.landscape * scale),
|
landscape: Math.round(scaleSize(TVPosterSizes.landscape) * scale),
|
||||||
episode: Math.round(TVPosterSizes.episode * scale),
|
episode: Math.round(scaleSize(TVPosterSizes.episode) * scale),
|
||||||
},
|
},
|
||||||
gaps: {
|
gaps: {
|
||||||
item: Math.round(TVGaps.item * scale),
|
item: Math.round(scaleSize(TVGaps.item) * scale),
|
||||||
section: Math.round(TVGaps.section * scale),
|
section: Math.round(scaleSize(TVGaps.section) * scale),
|
||||||
small: Math.round(TVGaps.small * scale),
|
small: Math.round(scaleSize(TVGaps.small) * scale),
|
||||||
large: Math.round(TVGaps.large * scale),
|
large: Math.round(scaleSize(TVGaps.large) * scale),
|
||||||
},
|
},
|
||||||
padding: {
|
padding: {
|
||||||
horizontal: Math.round(TVPadding.horizontal * scale),
|
horizontal: Math.round(scaleSize(TVPadding.horizontal) * scale),
|
||||||
scale: Math.round(TVPadding.scale * scale),
|
scale: Math.round(scaleSize(TVPadding.scale) * scale),
|
||||||
vertical: Math.round(TVPadding.vertical * scale),
|
vertical: Math.round(scaleSize(TVPadding.vertical) * scale),
|
||||||
heroHeight: TVPadding.heroHeight * scale,
|
heroHeight: TVPadding.heroHeight * scale,
|
||||||
},
|
},
|
||||||
animation: TVAnimation,
|
animation: TVAnimation,
|
||||||
|
|||||||
@@ -4,25 +4,28 @@ import { TVTypographyScale, useSettings } from "@/utils/atoms/settings";
|
|||||||
* TV Typography Scale
|
* TV Typography Scale
|
||||||
*
|
*
|
||||||
* Consistent text sizes for TV interface components.
|
* Consistent text sizes for TV interface components.
|
||||||
* These sizes are optimized for TV viewing distance.
|
* Design values are for 1920×1080 and scaled proportionally
|
||||||
|
* to the actual viewport via scaleSize().
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { scaleSize } from "@/utils/scaleSize";
|
||||||
|
|
||||||
export const TVTypography = {
|
export const TVTypography = {
|
||||||
/** Hero titles, movie/show names - 70px */
|
/** Hero titles, movie/show names */
|
||||||
display: 70,
|
display: scaleSize(70),
|
||||||
|
|
||||||
/** Episode series name, major headings - 42px */
|
/** Episode series name, major headings */
|
||||||
title: 42,
|
title: scaleSize(42),
|
||||||
|
|
||||||
/** Section headers (Cast, Technical Details, From this Series) - 32px */
|
/** Section headers (Cast, Technical Details, From this Series) */
|
||||||
heading: 32,
|
heading: scaleSize(32),
|
||||||
|
|
||||||
/** Overview, actor names, card titles, metadata - 20px */
|
/** Overview, actor names, card titles, metadata */
|
||||||
body: 20,
|
body: scaleSize(40),
|
||||||
|
|
||||||
/** Secondary text, labels, subtitles - 16px */
|
/** Secondary text, labels, subtitles */
|
||||||
callout: 16,
|
callout: scaleSize(26),
|
||||||
} as const;
|
};
|
||||||
|
|
||||||
export type TVTypographyKey = keyof typeof TVTypography;
|
export type TVTypographyKey = keyof typeof TVTypography;
|
||||||
|
|
||||||
|
|||||||
9
utils/scaleSize.ts
Normal file
9
utils/scaleSize.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Dimensions } from "react-native";
|
||||||
|
|
||||||
|
const { width: W, height: H } = Dimensions.get("window");
|
||||||
|
|
||||||
|
export const scaleSize = (size: number): number => {
|
||||||
|
const widthRatio = W / 1920;
|
||||||
|
const heightRatio = H / 1080;
|
||||||
|
return size * Math.min(widthRatio, heightRatio);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user