diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index 56ff0754..13c88a92 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -1175,6 +1175,11 @@ export default function page() { goToNextItem={goToNextItem} onRefreshSubtitleTracks={handleRefreshSubtitleTracks} addSubtitleFile={addSubtitleFile} + showTechnicalInfo={showTechnicalInfo} + onToggleTechnicalInfo={handleToggleTechnicalInfo} + getTechnicalInfo={getTechnicalInfo} + playMethod={playMethod} + transcodeReasons={transcodeReasons} /> ) : ( ; addSubtitleFile?: (path: string) => void; + showTechnicalInfo?: boolean; + onToggleTechnicalInfo?: () => void; + getTechnicalInfo?: () => Promise; + playMethod?: "DirectPlay" | "DirectStream" | "Transcode"; + transcodeReasons?: string[]; } const TV_SEEKBAR_HEIGHT = 14; @@ -97,6 +104,11 @@ export const Controls: FC = ({ goToNextItem: goToNextItemProp, onRefreshSubtitleTracks, addSubtitleFile, + showTechnicalInfo, + onToggleTechnicalInfo, + getTechnicalInfo, + playMethod, + transcodeReasons, }) => { const insets = useSafeAreaInsets(); const { t } = useTranslation(); @@ -127,7 +139,7 @@ export const Controls: FC = ({ const { showSubtitleModal } = useTVSubtitleModal(); // Track which button should have preferred focus when controls show - type LastModalType = "audio" | "subtitle" | null; + type LastModalType = "audio" | "subtitle" | "techInfo" | null; const [lastOpenedModal, setLastOpenedModal] = useState(null); // Track if play button should have focus (when showing controls via up/down D-pad) @@ -383,6 +395,12 @@ export const Controls: FC = ({ onRefreshSubtitleTracks, ]); + const handleToggleTechnicalInfo = useCallback(() => { + setLastOpenedModal("techInfo"); + onToggleTechnicalInfo?.(); + controlsInteractionRef.current(); + }, [onToggleTechnicalInfo]); + const effectiveProgress = useSharedValue(0); const SEEK_THRESHOLD_MS = 5000; @@ -763,6 +781,16 @@ export const Controls: FC = ({ pointerEvents='none' /> + {getTechnicalInfo && ( + + )} + {nextItem && ( = ({ hasTVPreferredFocus={!false && lastOpenedModal === "subtitle"} size={24} /> + + {getTechnicalInfo && ( + + )} {showSeekBubble && ( diff --git a/components/video-player/controls/TechnicalInfoOverlay.tsx b/components/video-player/controls/TechnicalInfoOverlay.tsx index 1498990d..fbd357f6 100644 --- a/components/video-player/controls/TechnicalInfoOverlay.tsx +++ b/components/video-player/controls/TechnicalInfoOverlay.tsx @@ -7,6 +7,7 @@ import Animated, { withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { TVTypography } from "@/constants/TVTypography"; import type { TechnicalInfo } from "@/modules/mpv-player"; import { useSettings } from "@/utils/atoms/settings"; import { HEADER_LAYOUT } from "./constants"; @@ -121,7 +122,7 @@ const formatTranscodeReason = (reason: string): string => { export const TechnicalInfoOverlay: FC = memo( ({ - showControls, + showControls: _showControls, visible, getTechnicalInfo, playMethod, @@ -168,64 +169,64 @@ export const TechnicalInfoOverlay: FC = memo( opacity: opacity.value, })); - // Hide on TV platforms - if (Platform.isTV) return null; - // Don't render if not visible if (!visible) return null; + // TV-specific styles + const containerStyle = Platform.isTV + ? { + top: Math.max(insets.top, 48) + 20, + left: Math.max(insets.left, 48) + 20, + } + : { + top: + (settings?.safeAreaInControlsEnabled ?? true) + ? insets.top + HEADER_LAYOUT.CONTAINER_PADDING + 4 + : HEADER_LAYOUT.CONTAINER_PADDING + 4, + left: + (settings?.safeAreaInControlsEnabled ?? true) + ? insets.left + HEADER_LAYOUT.CONTAINER_PADDING + 20 + : HEADER_LAYOUT.CONTAINER_PADDING + 20, + }; + + const textStyle = Platform.isTV ? styles.infoTextTV : styles.infoText; + const reasonStyle = Platform.isTV ? styles.reasonTextTV : styles.reasonText; + const boxStyle = Platform.isTV ? styles.infoBoxTV : styles.infoBox; + return ( - + {playMethod && ( {getPlayMethodLabel(playMethod)} )} {transcodeReasons && transcodeReasons.length > 0 && ( - + {transcodeReasons.map(formatTranscodeReason).join(", ")} )} {info?.videoWidth && info?.videoHeight && ( - + {info.videoWidth}x{info.videoHeight} )} {info?.videoCodec && ( - + Video: {formatCodec(info.videoCodec)} {info.fps ? ` @ ${formatFps(info.fps)} fps` : ""} )} {info?.audioCodec && ( - - Audio: {formatCodec(info.audioCodec)} - + Audio: {formatCodec(info.audioCodec)} )} {(info?.videoBitrate || info?.audioBitrate) && ( - + Bitrate:{" "} {info.videoBitrate ? formatBitrate(info.videoBitrate) @@ -235,18 +236,16 @@ export const TechnicalInfoOverlay: FC = memo( )} {info?.cacheSeconds !== undefined && ( - + Buffer: {info.cacheSeconds.toFixed(1)}s )} {info?.droppedFrames !== undefined && info.droppedFrames > 0 && ( - + Dropped: {info.droppedFrames} frames )} - {!info && !playMethod && ( - Loading... - )} + {!info && !playMethod && Loading...} ); @@ -267,12 +266,25 @@ const styles = StyleSheet.create({ paddingVertical: 8, minWidth: 150, }, + infoBoxTV: { + backgroundColor: "rgba(0, 0, 0, 0.6)", + borderRadius: 12, + paddingHorizontal: 20, + paddingVertical: 16, + minWidth: 250, + }, infoText: { color: "white", fontSize: 12, fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace", lineHeight: 18, }, + infoTextTV: { + color: "white", + fontSize: TVTypography.body, + fontFamily: Platform.OS === "ios" ? "Menlo" : "monospace", + lineHeight: TVTypography.body * 1.5, + }, warningText: { color: "#ff9800", }, @@ -280,4 +292,8 @@ const styles = StyleSheet.create({ color: "#fbbf24", fontSize: 10, }, + reasonTextTV: { + color: "#fbbf24", + fontSize: TVTypography.callout, + }, });