From ece5750d3460d5c1c5b13d14b4d39d78796c129a Mon Sep 17 00:00:00 2001 From: lance chant <13349722+lancechant@users.noreply.github.com> Date: Wed, 20 May 2026 07:56:39 +0200 Subject: [PATCH] Feat/tv interface uniform scale (#1562) Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app.json | 2 +- app/(auth)/(tabs)/_layout.tsx | 2 +- assets/icons/gear.png | Bin 0 -> 26160 bytes components/home/Home.tv.tsx | 26 ++++++- .../InfiniteScrollingCollectionList.tv.tsx | 40 +++++++---- .../StreamystatsPromotedWatchlists.tv.tsx | 19 ++--- .../home/StreamystatsRecommendations.tv.tsx | 5 +- components/home/TVHeroCarousel.tsx | 65 +++++++++++------- components/tv/TVPosterCard.tsx | 64 ++++++++++------- constants/TVSizes.ts | 26 +++---- constants/TVTypography.ts | 27 ++++---- .../modules/mpvplayer/MPVLayerRenderer.kt | 19 ++++- utils/scaleSize.ts | 9 +++ 13 files changed, 197 insertions(+), 107 deletions(-) create mode 100644 assets/icons/gear.png create mode 100644 utils/scaleSize.ts diff --git a/app.json b/app.json index f9dfbb6d..5974f6f1 100644 --- a/app.json +++ b/app.json @@ -85,7 +85,7 @@ "useFrameworks": "static" }, "android": { - "buildArchs": ["arm64-v8a", "x86_64"], + "buildArchs": ["arm64-v8a", "x86_64", "armeabi-v7a"], "compileSdkVersion": 36, "targetSdkVersion": 35, "buildToolsVersion": "35.0.0", diff --git a/app/(auth)/(tabs)/_layout.tsx b/app/(auth)/(tabs)/_layout.tsx index 1ed67eda..b92f3b13 100644 --- a/app/(auth)/(tabs)/_layout.tsx +++ b/app/(auth)/(tabs)/_layout.tsx @@ -134,7 +134,7 @@ export default function TabLayout() { tabBarItemHidden: !Platform.isTV, tabBarIcon: Platform.OS === "android" - ? (_e) => require("@/assets/icons/list.png") + ? (_e) => require("@/assets/icons/gear.png") //Should maybe use other libraries to have it uniform : (_e) => ({ sfSymbol: "gearshape.fill" }), }} /> diff --git a/assets/icons/gear.png b/assets/icons/gear.png new file mode 100644 index 0000000000000000000000000000000000000000..f5b98cf07201d2c8774795c792d6174b944ef928 GIT binary patch literal 26160 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 { const _invalidateCache = useInvalidatePlaybackProgressCache(); 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 const [focusedItem, setFocusedItem] = useState(null); const debounceTimerRef = useRef | null>(null); diff --git a/components/home/InfiniteScrollingCollectionList.tv.tsx b/components/home/InfiniteScrollingCollectionList.tv.tsx index ce32656d..64869ec1 100644 --- a/components/home/InfiniteScrollingCollectionList.tv.tsx +++ b/components/home/InfiniteScrollingCollectionList.tv.tsx @@ -24,9 +24,10 @@ import { useScaledTVTypography } from "@/constants/TVTypography"; import useRouter from "@/hooks/useAppRouter"; import { useTVItemActionModal } from "@/hooks/useTVItemActionModal"; import { SortByOption, SortOrderOption } from "@/utils/atoms/filters"; +import { scaleSize } from "@/utils/scaleSize"; // Extra padding to accommodate scale animation (1.05x) and glow shadow -const SCALE_PADDING = 20; +const SCALE_PADDING = scaleSize(20); interface Props extends ViewProps { title?: string | null; @@ -81,7 +82,7 @@ const TVSeeAllCard: React.FC<{ style={{ width, aspectRatio, - borderRadius: 24, + borderRadius: scaleSize(24), backgroundColor: "rgba(255, 255, 255, 0.08)", justifyContent: "center", alignItems: "center", @@ -91,9 +92,9 @@ const TVSeeAllCard: React.FC<{ > = ({ fontSize: typography.heading, fontWeight: "700", color: "#FFFFFF", - marginBottom: 20, + marginBottom: scaleSize(20), marginLeft: sizes.padding.horizontal, letterSpacing: 0.5, }} @@ -286,8 +287,8 @@ export const InfiniteScrollingCollectionList: React.FC = ({ backgroundColor: "#262626", width: itemWidth, aspectRatio: orientation === "horizontal" ? 16 / 9 : 10 / 15, - borderRadius: 12, - marginBottom: 8, + borderRadius: scaleSize(12), + marginBottom: scaleSize(8), }} /> = ({ removeClippedSubviews={false} maintainVisibleContentPosition={{ minIndexForVisible: 0 }} style={{ overflow: "visible" }} - contentInset={{ - left: sizes.padding.horizontal, - right: sizes.padding.horizontal, - }} - contentOffset={{ x: -sizes.padding.horizontal, y: 0 }} contentContainerStyle={{ - paddingVertical: SCALE_PADDING, + paddingVertical: sizes.gaps.small, + paddingLeft: sizes.padding.horizontal, + paddingRight: sizes.padding.horizontal, }} + // Below is a work around with the contentInset, same in TVHeroCarousel, if okay on apple remove + // ListHeaderComponent={ + // + // } + // contentInset={{ + // left: sizes.padding.horizontal, + // right: sizes.padding.horizontal, + // }} + // contentOffset={{ x: -sizes.padding.horizontal, y: 0 }} + // contentContainerStyle={{ paddingVertical: SCALE_PADDING }} ListFooterComponent={ {isFetchingNextPage && ( @@ -350,7 +359,10 @@ export const InfiniteScrollingCollectionList: React.FC = ({ marginLeft: itemWidth / 2, marginRight: ITEM_GAP, justifyContent: "center", - height: orientation === "horizontal" ? 191 : 315, + height: + orientation === "horizontal" + ? scaleSize(191) + : scaleSize(315), }} > diff --git a/components/home/StreamystatsPromotedWatchlists.tv.tsx b/components/home/StreamystatsPromotedWatchlists.tv.tsx index f2ae66a8..27730a1e 100644 --- a/components/home/StreamystatsPromotedWatchlists.tv.tsx +++ b/components/home/StreamystatsPromotedWatchlists.tv.tsx @@ -19,10 +19,11 @@ import useRouter from "@/hooks/useAppRouter"; import { useTVItemActionModal } from "@/hooks/useTVItemActionModal"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; +import { scaleSize } from "@/utils/scaleSize"; import { createStreamystatsApi } from "@/utils/streamystats/api"; import type { StreamystatsWatchlist } from "@/utils/streamystats/types"; -const SCALE_PADDING = 20; +const SCALE_PADDING = scaleSize(20); interface WatchlistSectionProps extends ViewProps { watchlist: StreamystatsWatchlist; @@ -168,8 +169,8 @@ const WatchlistSection: React.FC = ({ backgroundColor: "#262626", width: posterSizes.poster, aspectRatio: 10 / 15, - borderRadius: 12, - marginBottom: 8, + borderRadius: scaleSize(12), + marginBottom: scaleSize(8), }} /> @@ -286,12 +287,12 @@ export const StreamystatsPromotedWatchlists: React.FC< diff --git a/components/home/StreamystatsRecommendations.tv.tsx b/components/home/StreamystatsRecommendations.tv.tsx index a35da259..151d37a2 100644 --- a/components/home/StreamystatsRecommendations.tv.tsx +++ b/components/home/StreamystatsRecommendations.tv.tsx @@ -18,6 +18,7 @@ import useRouter from "@/hooks/useAppRouter"; import { useTVItemActionModal } from "@/hooks/useTVItemActionModal"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; +import { scaleSize } from "@/utils/scaleSize"; import { createStreamystatsApi } from "@/utils/streamystats/api"; import type { StreamystatsRecommendationsIdsResponse } from "@/utils/streamystats/types"; @@ -220,8 +221,8 @@ export const StreamystatsRecommendations: React.FC = ({ backgroundColor: "#262626", width: sizes.posters.poster, aspectRatio: 10 / 15, - borderRadius: 12, - marginBottom: 8, + borderRadius: scaleSize(12), + marginBottom: scaleSize(8), }} /> diff --git a/components/home/TVHeroCarousel.tsx b/components/home/TVHeroCarousel.tsx index 0d5b6bac..7a71fbfb 100644 --- a/components/home/TVHeroCarousel.tsx +++ b/components/home/TVHeroCarousel.tsx @@ -33,6 +33,7 @@ import { import { apiAtom } from "@/providers/JellyfinProvider"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; +import { scaleSize } from "@/utils/scaleSize"; import { runtimeTicksToMinutes } from "@/utils/time"; const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window"); @@ -129,7 +130,7 @@ const HeroCard: React.FC = React.memo( = React.memo( style={{ width: sizes.posters.episode, aspectRatio: 16 / 9, - borderRadius: 24, + borderRadius: scaleSize(24), overflow: "hidden", transform: [{ scale }], - borderWidth: 2, + borderWidth: scaleSize(2), borderColor: focused ? "#FFFFFF" : "transparent", shadowColor: "#FFFFFF", shadowOffset: { width: 0, height: 0 }, shadowOpacity: focused ? 0.6 : 0, - shadowRadius: focused ? 20 : 0, + shadowRadius: focused ? scaleSize(20) : 0, }} > {posterUrl ? ( @@ -183,7 +184,7 @@ const HeroCard: React.FC = React.memo( > @@ -472,7 +473,10 @@ export const TVHeroCarousel: React.FC = ({ left: sizes.padding.horizontal, right: sizes.padding.horizontal, bottom: - 40 + sizes.posters.episode * (9 / 16) + sizes.gaps.small * 2 + 20, + scaleSize(40) + + sizes.posters.episode * (9 / 16) + + sizes.gaps.small * 2 + + scaleSize(20), }} > {/* Logo or Title */} @@ -480,9 +484,9 @@ export const TVHeroCarousel: React.FC = ({ = ({ fontSize: typography.display, fontWeight: "bold", color: "#FFFFFF", - marginBottom: 12, + marginBottom: scaleSize(12), }} numberOfLines={1} > @@ -507,7 +511,7 @@ export const TVHeroCarousel: React.FC = ({ style={{ fontSize: typography.body, color: "rgba(255,255,255,0.9)", - marginBottom: 12, + marginBottom: scaleSize(12), }} numberOfLines={1} > @@ -521,7 +525,7 @@ export const TVHeroCarousel: React.FC = ({ style={{ fontSize: typography.body, color: "rgba(255,255,255,0.8)", - marginBottom: 16, + marginBottom: scaleSize(16), maxWidth: SCREEN_WIDTH * 0.5, lineHeight: typography.body * 1.4, }} @@ -536,7 +540,7 @@ export const TVHeroCarousel: React.FC = ({ style={{ flexDirection: "row", alignItems: "center", - gap: 16, + gap: scaleSize(16), }} > {year && ( @@ -562,10 +566,10 @@ export const TVHeroCarousel: React.FC = ({ {activeItem?.OfficialRating && ( @@ -584,15 +588,15 @@ export const TVHeroCarousel: React.FC = ({ style={{ flexDirection: "row", alignItems: "center", - gap: 6, + gap: scaleSize(6), }} > @@ -624,7 +628,7 @@ export const TVHeroCarousel: React.FC = ({ position: "absolute", left: 0, right: 0, - bottom: 40, + bottom: scaleSize(40), }} > = ({ keyExtractor={keyExtractor} showsHorizontalScrollIndicator={false} style={{ overflow: "visible" }} - contentInset={{ - left: sizes.padding.horizontal, - right: sizes.padding.horizontal, + contentContainerStyle={{ + paddingVertical: sizes.gaps.small, + paddingLeft: sizes.padding.horizontal, + paddingRight: sizes.padding.horizontal, }} - contentOffset={{ x: -sizes.padding.horizontal, y: 0 }} - contentContainerStyle={{ paddingVertical: sizes.gaps.small }} + // Below is a work around with the contentInset, same in infiniteScrollingCollectionList, if okay on apple remove + // ListHeaderComponent={ + // + // } + // contentInset={{ + // left: sizes.padding.horizontal, + // right: sizes.padding.horizontal, + // }} + // contentOffset={{ x: -sizes.padding.horizontal, y: 0 }} + // contentContainerStyle={{ paddingVertical: sizes.gaps.small }} renderItem={renderHeroCard} removeClippedSubviews={false} initialNumToRender={8} diff --git a/components/tv/TVPosterCard.tsx b/components/tv/TVPosterCard.tsx index 6cb4fa82..83274ea3 100644 --- a/components/tv/TVPosterCard.tsx +++ b/components/tv/TVPosterCard.tsx @@ -21,6 +21,7 @@ import { } from "@/modules/glass-poster"; import { apiAtom } from "@/providers/JellyfinProvider"; import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl"; +import { scaleSize } from "@/utils/scaleSize"; import { runtimeTicksToMinutes } from "@/utils/time"; export interface TVPosterCardProps { @@ -225,7 +226,13 @@ export const TVPosterCard: React.FC = ({ : null; return ( - + {episodeLabel && ( = ({ style={{ fontSize: typography.callout, color: "#9CA3AF", - marginTop: 4, + marginTop: scaleSize(4), }} > {item.ChannelName} @@ -277,7 +284,7 @@ export const TVPosterCard: React.FC = ({ style={{ fontSize: typography.callout, color: "#9CA3AF", - marginTop: 4, + marginTop: scaleSize(4), }} > {artist} @@ -296,7 +303,7 @@ export const TVPosterCard: React.FC = ({ style={{ fontSize: typography.callout, color: "#9CA3AF", - marginTop: 4, + marginTop: scaleSize(4), }} > {artist} @@ -312,7 +319,7 @@ export const TVPosterCard: React.FC = ({ style={{ fontSize: typography.callout, color: "#9CA3AF", - marginTop: 4, + marginTop: scaleSize(4), }} > {item.ChildCount} tracks @@ -328,7 +335,7 @@ export const TVPosterCard: React.FC = ({ style={{ fontSize: typography.callout, color: "#9CA3AF", - marginTop: 4, + marginTop: scaleSize(4), }} > {item.ProductionYear} @@ -344,23 +351,23 @@ export const TVPosterCard: React.FC = ({ - + @@ -382,7 +389,7 @@ export const TVPosterCard: React.FC = ({ justifyContent: "center", }} > - + ) : null; @@ -395,9 +402,9 @@ export const TVPosterCard: React.FC = ({ style={{ width, aspectRatio, - borderRadius: 24, + borderRadius: scaleSize(24), backgroundColor: "#1a1a1a", - borderWidth: 2, + borderWidth: scaleSize(2), borderColor: focused ? "#FFFFFF" : "transparent", }} /> @@ -411,7 +418,7 @@ export const TVPosterCard: React.FC = ({ = ({ position: "relative", width, aspectRatio, - borderRadius: 24, + borderRadius: scaleSize(4), overflow: "hidden", backgroundColor: "#1a1a1a", - borderWidth: 2, + borderWidth: scaleSize(2), borderColor: focused ? "#FFFFFF" : "transparent", }} > @@ -470,7 +477,7 @@ export const TVPosterCard: React.FC = ({ style={{ fontSize: typography.callout, color: "#FFFFFF", - marginTop: 4, + marginTop: scaleSize(4), fontWeight: "500", }} > @@ -498,8 +505,13 @@ export const TVPosterCard: React.FC = ({ // Default: show name return ( {item.Name} @@ -551,7 +563,7 @@ export const TVPosterCard: React.FC = ({ shadowColor: useGlass ? undefined : shadowColor, shadowOffset: useGlass ? undefined : { width: 0, height: 0 }, shadowOpacity: useGlass ? undefined : focused ? 0.3 : 0, - shadowRadius: useGlass ? undefined : focused ? 12 : 0, + shadowRadius: useGlass ? undefined : focused ? scaleSize(12) : 0, }} > {renderPosterImage()} @@ -560,7 +572,9 @@ export const TVPosterCard: React.FC = ({ {/* Text below poster */} {showText && ( - + {item.Type === "Episode" ? ( <> {renderSubtitle()} diff --git a/constants/TVSizes.ts b/constants/TVSizes.ts index 20c38daa..676609bc 100644 --- a/constants/TVSizes.ts +++ b/constants/TVSizes.ts @@ -1,10 +1,12 @@ import { TVTypographyScale, useSettings } from "@/utils/atoms/settings"; +import { scaleSize } from "@/utils/scaleSize"; /** * TV Layout Sizes * * 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 = { /** Horizontal padding from screen edges */ - horizontal: 60, + horizontal: 90, /** Padding to accommodate scale animations (1.05x) */ scale: 20, @@ -129,20 +131,20 @@ export const useScaledTVSizes = (): ScaledTVSizes => { return { posters: { - poster: Math.round(TVPosterSizes.poster * scale), - landscape: Math.round(TVPosterSizes.landscape * scale), - episode: Math.round(TVPosterSizes.episode * scale), + poster: Math.round(scaleSize(TVPosterSizes.poster) * scale), + landscape: Math.round(scaleSize(TVPosterSizes.landscape) * scale), + episode: Math.round(scaleSize(TVPosterSizes.episode) * scale), }, gaps: { - item: Math.round(TVGaps.item * scale), - section: Math.round(TVGaps.section * scale), - small: Math.round(TVGaps.small * scale), - large: Math.round(TVGaps.large * scale), + item: Math.round(scaleSize(TVGaps.item) * scale), + section: Math.round(scaleSize(TVGaps.section) * scale), + small: Math.round(scaleSize(TVGaps.small) * scale), + large: Math.round(scaleSize(TVGaps.large) * scale), }, padding: { - horizontal: Math.round(TVPadding.horizontal * scale), - scale: Math.round(TVPadding.scale * scale), - vertical: Math.round(TVPadding.vertical * scale), + horizontal: Math.round(scaleSize(TVPadding.horizontal) * scale), + scale: Math.round(scaleSize(TVPadding.scale) * scale), + vertical: Math.round(scaleSize(TVPadding.vertical) * scale), heroHeight: TVPadding.heroHeight * scale, }, animation: TVAnimation, diff --git a/constants/TVTypography.ts b/constants/TVTypography.ts index a2ac3b80..833f617e 100644 --- a/constants/TVTypography.ts +++ b/constants/TVTypography.ts @@ -4,25 +4,28 @@ import { TVTypographyScale, useSettings } from "@/utils/atoms/settings"; * TV Typography Scale * * 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 = { - /** Hero titles, movie/show names - 70px */ - display: 70, + /** Hero titles, movie/show names */ + display: scaleSize(70), - /** Episode series name, major headings - 42px */ - title: 42, + /** Episode series name, major headings */ + title: scaleSize(42), - /** Section headers (Cast, Technical Details, From this Series) - 32px */ - heading: 32, + /** Section headers (Cast, Technical Details, From this Series) */ + heading: scaleSize(32), - /** Overview, actor names, card titles, metadata - 20px */ - body: 20, + /** Overview, actor names, card titles, metadata */ + body: scaleSize(40), - /** Secondary text, labels, subtitles - 16px */ - callout: 16, -} as const; + /** Secondary text, labels, subtitles */ + callout: scaleSize(26), +}; export type TVTypographyKey = keyof typeof TVTypography; diff --git a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt index 38c55625..8860932e 100644 --- a/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt +++ b/modules/mpv-player/android/src/main/java/expo/modules/mpvplayer/MPVLayerRenderer.kt @@ -1,6 +1,8 @@ package expo.modules.mpvplayer +import android.app.UiModeManager import android.content.Context +import android.content.res.Configuration import android.content.res.AssetManager import android.os.Handler import android.os.Looper @@ -27,7 +29,12 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { const val MPV_FORMAT_DOUBLE = 5 const val MPV_FORMAT_NODE = 6 } - + + private fun isTvDevice(): Boolean { + val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + return uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION + } + interface Delegate { fun onPositionChanged(position: Double, duration: Double, cacheSeconds: Double) fun onPauseChanged(isPaused: Boolean) @@ -157,7 +164,15 @@ class MPVLayerRenderer(private val context: Context) : MPVLib.EventObserver { MPVLib.setOptionString("opengl-es", "yes") // Hardware video decoding - MPVLib.setOptionString("hwdec", "mediacodec-copy") + // TV: zero-copy (mediacodec) for better performance on low-power devices + // Mobile: copy mode (mediacodec-copy) for better compatibility + val isTV = isTvDevice() + if (isTV) { + MPVLib.setOptionString("hwdec", "mediacodec") + MPVLib.setOptionString("profile", "fast") + } else { + MPVLib.setOptionString("hwdec", "mediacodec-copy") + } MPVLib.setOptionString("hwdec-codecs", "h264,hevc,mpeg4,mpeg2video,vp8,vp9,av1") // Cache settings for better network streaming diff --git a/utils/scaleSize.ts b/utils/scaleSize.ts new file mode 100644 index 00000000..09cc9a56 --- /dev/null +++ b/utils/scaleSize.ts @@ -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); +};