diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index fe2fdd56..7a2e1781 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -154,6 +154,13 @@ export default function page() { : BITRATES[0].value; const [item, setItem] = useState(null); + const initialSeekDoneRef = useRef(false); + + const initialPlaybackTicksRef = useRef( + playbackPositionFromUrl + ? Number.parseInt(playbackPositionFromUrl, 10) + : (item?.UserData?.PlaybackPositionTicks ?? 0), + ); const [downloadedItem, setDownloadedItem] = useState( null, ); @@ -214,12 +221,25 @@ export default function page() { ); /** Gets the initial playback position from the URL. */ - const getInitialPlaybackTicks = useCallback((): number => { - if (playbackPositionFromUrl) { - return Number.parseInt(playbackPositionFromUrl, 10); + // const getInitialPlaybackTicks = useCallback((): number => { + // if (playbackPositionFromUrl) { + // return Number.parseInt(playbackPositionFromUrl, 10); + // } + // return item?.UserData?.PlaybackPositionTicks ?? 0; + // }, [playbackPositionFromUrl, item?.UserData?.PlaybackPositionTicks]); + + useEffect(() => { + if (!tracksReady || !videoRef.current) return; + if (initialSeekDoneRef.current) return; + + initialSeekDoneRef.current = true; + + const ticks = initialPlaybackTicksRef.current; + + if (ticks > 0) { + videoRef.current.seekTo(ticksToSeconds(ticks)); } - return item?.UserData?.PlaybackPositionTicks ?? 0; - }, [playbackPositionFromUrl, item?.UserData?.PlaybackPositionTicks]); + }, [tracksReady]); useEffect(() => { const fetchItemData = async () => { @@ -588,6 +608,11 @@ export default function page() { : (item?.UserData?.PlaybackPositionTicks ?? 0); const startPos = ticksToSeconds(startTicks); + console.log( + `[DirectPlayer] Resume position — ticks: ${startTicks}, seconds: ${startPos}, ` + + `fromUrl: ${playbackPositionFromUrl}, itemTicks: ${item?.UserData?.PlaybackPositionTicks ?? 0}`, + ); + // Build source config - headers only needed for online streaming const source: MpvVideoSource = { url: stream.url, @@ -743,7 +768,7 @@ export default function page() { currentPlayStateInfo() as PlaybackProgressInfo, ); } - if (!Platform.isTV) await activateKeepAwakeAsync(); + await activateKeepAwakeAsync(); return; } @@ -756,7 +781,7 @@ export default function page() { currentPlayStateInfo() as PlaybackProgressInfo, ); } - if (!Platform.isTV) await deactivateKeepAwake(); + await deactivateKeepAwake(); return; } @@ -1099,13 +1124,6 @@ export default function page() { applySubtitleSettings(); }, [isVideoLoaded, settings]); - // Seek to resume position after file is loaded (MPV_EVENT_FILE_LOADED) - useEffect(() => { - if (!tracksReady || !videoRef.current) return; - const ticks = getInitialPlaybackTicks(); - videoRef.current?.seekTo?.(ticksToSeconds(ticks)); - }, [tracksReady, getInitialPlaybackTicks]); - // Apply initial playback speed when video loads useEffect(() => { if (!isVideoLoaded || !videoRef.current) return; diff --git a/components/tv/TVHorizontalList.tsx b/components/tv/TVHorizontalList.tsx index 87a2db73..4bb61168 100644 --- a/components/tv/TVHorizontalList.tsx +++ b/components/tv/TVHorizontalList.tsx @@ -3,6 +3,7 @@ import { FlatList, ScrollView, View } from "react-native"; import { Text } from "@/components/common/Text"; import { useScaledTVSizes } from "@/constants/TVSizes"; import { useScaledTVTypography } from "@/constants/TVTypography"; +import { scaleSize } from "@/utils/scaleSize"; interface TVHorizontalListProps { /** Data items to render */ @@ -94,7 +95,7 @@ export function TVHorizontalList({ fontSize: typography.heading, fontWeight: "700", color: "#FFFFFF", - marginBottom: 20, + marginBottom: scaleSize(20), marginLeft: sizes.padding.scale, letterSpacing: 0.5, }} @@ -125,7 +126,7 @@ export function TVHorizontalList({ fontSize: typography.heading, fontWeight: "700", color: "#FFFFFF", - marginBottom: 20, + marginBottom: scaleSize(20), marginLeft: sizes.padding.scale, letterSpacing: 0.5, }} @@ -167,7 +168,7 @@ export function TVHorizontalList({ fontSize: typography.heading, fontWeight: "700", color: "#FFFFFF", - marginBottom: 20, + marginBottom: scaleSize(20), marginLeft: sizes.padding.scale, letterSpacing: 0.5, }} diff --git a/components/tv/TVItemCardText.tsx b/components/tv/TVItemCardText.tsx index fe507704..c55110b3 100644 --- a/components/tv/TVItemCardText.tsx +++ b/components/tv/TVItemCardText.tsx @@ -3,6 +3,7 @@ import React from "react"; import { View } from "react-native"; import { Text } from "@/components/common/Text"; import { useScaledTVTypography } from "@/constants/TVTypography"; +import { scaleSize } from "@/utils/scaleSize"; export interface TVItemCardTextProps { item: BaseItemDto; @@ -12,7 +13,7 @@ export const TVItemCardText: React.FC = ({ item }) => { const typography = useScaledTVTypography(); return ( - + = ({ item }) => { style={{ fontSize: typography.callout - 2, color: "#9CA3AF", - marginTop: 2, + marginTop: scaleSize(2), }} > {item.ProductionYear} diff --git a/components/tv/TVLanguageCard.tsx b/components/tv/TVLanguageCard.tsx index 18221ea5..027a1ada 100644 --- a/components/tv/TVLanguageCard.tsx +++ b/components/tv/TVLanguageCard.tsx @@ -91,7 +91,7 @@ const createStyles = (typography: ReturnType) => }, languageCardCode: { fontSize: typography.callout, - marginTop: 2, + marginTop: scaleSize(2), }, checkmark: { position: "absolute", diff --git a/components/tv/TVNextEpisodeCountdown.tsx b/components/tv/TVNextEpisodeCountdown.tsx index 4c045d5e..e6360875 100644 --- a/components/tv/TVNextEpisodeCountdown.tsx +++ b/components/tv/TVNextEpisodeCountdown.tsx @@ -45,8 +45,8 @@ export interface TVNextEpisodeCountdownProps { } // Position constants -const BOTTOM_WITH_CONTROLS = 300; -const BOTTOM_WITHOUT_CONTROLS = 120; +const BOTTOM_WITH_CONTROLS = scaleSize(300); +const BOTTOM_WITHOUT_CONTROLS = scaleSize(120); export const TVNextEpisodeCountdown: FC = ({ nextItem, @@ -75,7 +75,7 @@ export const TVNextEpisodeCountdown: FC = ({ const imageUrl = getPrimaryImageUrl({ api, item: nextItem, - width: 360, + width: scaleSize(360), quality: 80, }); @@ -244,15 +244,15 @@ const createStyles = (typography: ReturnType) => marginBottom: scaleSize(12), }, progressContainer: { - height: 4, + height: scaleSize(4), backgroundColor: "rgba(255,255,255,0.2)", - borderRadius: 2, + borderRadius: scaleSize(2), overflow: "hidden", }, progressBar: { height: "100%", backgroundColor: "#fff", - borderRadius: 2, + borderRadius: scaleSize(2), }, returnFocusGuide: { height: 1, diff --git a/components/tv/TVOptionButton.tsx b/components/tv/TVOptionButton.tsx index 562bd634..1a3ee51d 100644 --- a/components/tv/TVOptionButton.tsx +++ b/components/tv/TVOptionButton.tsx @@ -3,6 +3,7 @@ import React from "react"; import { Animated, Pressable, View } from "react-native"; import { Text } from "@/components/common/Text"; import { useScaledTVTypography } from "@/constants/TVTypography"; +import { scaleSize } from "@/utils/scaleSize"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; export interface TVOptionButtonProps { @@ -42,12 +43,12 @@ export const TVOptionButton = React.forwardRef( diff --git a/components/tv/TVOptionCard.tsx b/components/tv/TVOptionCard.tsx index 854d2b18..525a7b62 100644 --- a/components/tv/TVOptionCard.tsx +++ b/components/tv/TVOptionCard.tsx @@ -78,7 +78,7 @@ export const TVOptionCard = React.forwardRef( fontSize: typography.callout, color: focused ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)", textAlign: "center", - marginTop: 2, + marginTop: scaleSize(2), }} numberOfLines={1} > diff --git a/components/tv/TVProgressBar.tsx b/components/tv/TVProgressBar.tsx index 026b9247..e8ee60f8 100644 --- a/components/tv/TVProgressBar.tsx +++ b/components/tv/TVProgressBar.tsx @@ -1,5 +1,6 @@ import React from "react"; import { View } from "react-native"; +import { scaleSize } from "@/utils/scaleSize"; export interface TVProgressBarProps { /** Progress value between 0 and 1 */ @@ -23,14 +24,16 @@ export const TVProgressBar: React.FC = React.memo( height = 4, }) => { const clampedProgress = Math.max(0, Math.min(1, progress)); + const scaledMaxWidth = scaleSize(maxWidth); + const scaledHeight = scaleSize(height); return ( - + @@ -39,7 +42,7 @@ export const TVProgressBar: React.FC = React.memo( width: `${clampedProgress * 100}%`, height: "100%", backgroundColor: fillColor, - borderRadius: height / 2, + borderRadius: scaledHeight / 2, }} /> diff --git a/components/tv/TVSeriesNavigation.tsx b/components/tv/TVSeriesNavigation.tsx index 5414dde7..4f828a4e 100644 --- a/components/tv/TVSeriesNavigation.tsx +++ b/components/tv/TVSeriesNavigation.tsx @@ -5,6 +5,7 @@ import { ScrollView, View } from "react-native"; import { Text } from "@/components/common/Text"; import { useScaledTVSizes } from "@/constants/TVSizes"; import { useScaledTVTypography } from "@/constants/TVTypography"; +import { scaleSize } from "@/utils/scaleSize"; import { TVSeriesSeasonCard } from "./TVSeriesSeasonCard"; export interface TVSeriesNavigationProps { @@ -33,7 +34,7 @@ export const TVSeriesNavigation: React.FC = React.memo( fontSize: typography.heading, fontWeight: "700", color: "#FFFFFF", - marginBottom: 20, + marginBottom: scaleSize(20), letterSpacing: 0.5, }} > diff --git a/components/tv/TVSubtitleResultCard.tsx b/components/tv/TVSubtitleResultCard.tsx index e1fe7ebe..f2688529 100644 --- a/components/tv/TVSubtitleResultCard.tsx +++ b/components/tv/TVSubtitleResultCard.tsx @@ -240,12 +240,12 @@ const createStyles = (typography: ReturnType) => ratingContainer: { flexDirection: "row", alignItems: "center", - gap: 3, + gap: scaleSize(3), }, downloadCountContainer: { flexDirection: "row", alignItems: "center", - gap: 3, + gap: scaleSize(3), }, flagsContainer: { flexDirection: "row", diff --git a/components/tv/TVTabButton.tsx b/components/tv/TVTabButton.tsx index d545985b..be8ea8c2 100644 --- a/components/tv/TVTabButton.tsx +++ b/components/tv/TVTabButton.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Animated, Pressable } from "react-native"; import { Text } from "@/components/common/Text"; import { useScaledTVTypography } from "@/constants/TVTypography"; +import { scaleSize } from "@/utils/scaleSize"; import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation"; export interface TVTabButtonProps { @@ -48,10 +49,10 @@ export const TVTabButton: React.FC = ({ ? "rgba(255,255,255,0.2)" : "transparent", borderBottomColor: active ? "#fff" : "transparent", - borderBottomWidth: 2, - paddingHorizontal: 20, - paddingVertical: 12, - borderRadius: 8, + borderBottomWidth: scaleSize(2), + paddingHorizontal: scaleSize(20), + paddingVertical: scaleSize(12), + borderRadius: scaleSize(8), }, ]} > diff --git a/components/tv/TVThemeMusicIndicator.tsx b/components/tv/TVThemeMusicIndicator.tsx index bdf1041a..88fd1e1a 100644 --- a/components/tv/TVThemeMusicIndicator.tsx +++ b/components/tv/TVThemeMusicIndicator.tsx @@ -66,10 +66,10 @@ export const TVThemeMusicIndicator: React.FC = ({ )} diff --git a/components/tv/TVTrackCard.tsx b/components/tv/TVTrackCard.tsx index 358035d3..b2b45162 100644 --- a/components/tv/TVTrackCard.tsx +++ b/components/tv/TVTrackCard.tsx @@ -96,7 +96,7 @@ const createStyles = (typography: ReturnType) => }, trackCardSublabel: { fontSize: typography.callout, - marginTop: 2, + marginTop: scaleSize(2), }, checkmark: { position: "absolute", diff --git a/components/tv/TVUserCard.tsx b/components/tv/TVUserCard.tsx index 550a91e4..b687bf75 100644 --- a/components/tv/TVUserCard.tsx +++ b/components/tv/TVUserCard.tsx @@ -120,10 +120,14 @@ export const TVUserCard = React.forwardRef( {/* Text column */} - + {/* Username */} ( style={{ flexDirection: "row", alignItems: "center", - gap: 4, + gap: scaleSize(4), }} > = ({ null, ); + // Ref for the invisible focus-stealing overlay (prevents hidden buttons from receiving select events) + const focusOverlayRef = useRef(null); + const audioTracks = useMemo(() => { return mediaSource?.MediaStreams?.filter((s) => s.Type === "Audio") ?? []; }, [mediaSource]); @@ -908,6 +912,33 @@ export const Controls: FC = ({ setFocusPlayButton(false); }, [setShowControls]); + // On initial mount when controls start visible, focus the play button. + // playButtonRef transitions from null → View on first render; once set, + // this effect won't re-fire (playButtonRef is a stable reference). + const initialFocusDone = useRef(false); + useEffect(() => { + if (!initialFocusDone.current && playButtonRef && showControls) { + initialFocusDone.current = true; + const t = setTimeout(() => { + playButtonRef.focus(); + }, 100); + return () => clearTimeout(t); + } + }, [showControls, playButtonRef]); + + // When controls hide (and no skip/countdown overlay is visible), move focus + // to the invisible overlay so hidden buttons can't receive select events. + useEffect(() => { + if (!showControls && !isSkipOrCountdownVisible) { + // Small delay to let the controls fade-out animation start and + // the focus engine settle before stealing focus + const t = setTimeout(() => { + focusOverlayRef.current?.focus(); + }, 100); + return () => clearTimeout(t); + } + }, [showControls, isSkipOrCountdownVisible]); + const handleBack = useCallback(() => { router.back(); }, [router]); @@ -1025,6 +1056,24 @@ export const Controls: FC = ({ pointerEvents='none' /> + {/* Invisible overlay that steals focus when controls are hidden. + Prevents hidden control buttons from receiving select/enter events + from the TV remote. Pressing center button here toggles play/pause. */} + { + togglePlay(); + setShowControls(true); + setFocusPlayButton(true); + }} + /> + {getTechnicalInfo && ( = ({ = { - [TVTypographyScale.Small]: 0.9, - [TVTypographyScale.Default]: 1.0, - [TVTypographyScale.Large]: 1.1, - [TVTypographyScale.ExtraLarge]: 1.2, + [TVTypographyScale.Small]: 0.63, + [TVTypographyScale.Default]: 0.7, + [TVTypographyScale.Large]: 0.77, + [TVTypographyScale.ExtraLarge]: 0.84, }; // ============================================================================= diff --git a/constants/TVTypography.ts b/constants/TVTypography.ts index 9c8d2b14..cbac9b69 100644 --- a/constants/TVTypography.ts +++ b/constants/TVTypography.ts @@ -37,10 +37,10 @@ export type TVTypographyKey = keyof typeof TVTypography; // ============================================================================= const scaleMultipliers: Record = { - [TVTypographyScale.Small]: 0.85, - [TVTypographyScale.Default]: 1.0, - [TVTypographyScale.Large]: 1.2, - [TVTypographyScale.ExtraLarge]: 1.4, + [TVTypographyScale.Small]: 0.6, + [TVTypographyScale.Default]: 0.7, + [TVTypographyScale.Large]: 0.84, + [TVTypographyScale.ExtraLarge]: 0.98, }; // =============================================================================