From ec49d03cf13e84b96b94d95218198fa61e04be8f Mon Sep 17 00:00:00 2001 From: Uruk Date: Fri, 22 May 2026 00:59:57 +0200 Subject: [PATCH] refactor(casting): extract CastPlayerPoster --- app/(auth)/casting-player.tsx | 137 ++------------ .../casting/player/CastPlayerPoster.tsx | 176 ++++++++++++++++++ 2 files changed, 189 insertions(+), 124 deletions(-) create mode 100644 components/casting/player/CastPlayerPoster.tsx diff --git a/app/(auth)/casting-player.tsx b/app/(auth)/casting-player.tsx index d1735bd4d..ac40646b2 100644 --- a/app/(auth)/casting-player.tsx +++ b/app/(auth)/casting-player.tsx @@ -37,6 +37,7 @@ import Animated, { import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BITRATES } from "@/components/BitrateSelector"; import { CastPlayerHeader } from "@/components/casting/player/CastPlayerHeader"; +import { CastPlayerPoster } from "@/components/casting/player/CastPlayerPoster"; import { CastPlayerTitle } from "@/components/casting/player/CastPlayerTitle"; import { ChromecastDeviceSheet } from "@/components/chromecast/ChromecastDeviceSheet"; import { ChromecastEpisodeList } from "@/components/chromecast/ChromecastEpisodeList"; @@ -695,130 +696,18 @@ export default function CastingPlayerScreen() { showsVerticalScrollIndicator={false} > {/* Poster with buffering overlay */} - - - {posterUrl ? ( - - ) : ( - - - - )} - - {/* Skip intro/credits bar at bottom of poster */} - {currentSegment && ( - { - if (!remoteMediaClient) return; - try { - const seekFn = async (positionMs: number) => { - if ( - mediaStatus?.playerState === - MediaPlayerState.PLAYING || - mediaStatus?.playerState === MediaPlayerState.PAUSED - ) { - await remoteMediaClient.seek({ - position: positionMs / 1000, - }); - } - }; - if (currentSegment.type === "intro") { - await skipIntro(seekFn); - } else if (currentSegment.type === "credits") { - await skipCredits(seekFn); - } else { - await skipSegment(seekFn); - } - } catch (error) { - console.error("[Casting Player] Skip error:", error); - } - }} - style={{ - position: "absolute", - bottom: 0, - left: 0, - right: 0, - backgroundColor: protocolColor, - paddingVertical: 12, - paddingHorizontal: 16, - flexDirection: "row", - alignItems: "center", - justifyContent: "center", - gap: 8, - }} - > - - - {t( - `player.skip_${currentSegment.type === "credits" ? "outro" : currentSegment.type}`, - )} - - - )} - - {/* Buffering overlay */} - {isBuffering && ( - - - - {t("casting_player.buffering")} - - - )} - - + {/* Fixed 4-button control row for episodes - positioned independently */} diff --git a/components/casting/player/CastPlayerPoster.tsx b/components/casting/player/CastPlayerPoster.tsx new file mode 100644 index 000000000..aa5e8c3d5 --- /dev/null +++ b/components/casting/player/CastPlayerPoster.tsx @@ -0,0 +1,176 @@ +/** + * Casting Player Poster + * Poster image with empty-state fallback, skip intro/credits bar, and buffering overlay. + */ + +import { Ionicons } from "@expo/vector-icons"; +import { Image } from "expo-image"; +import type { TFunction } from "i18next"; +import { ActivityIndicator, Pressable, View } from "react-native"; +import { + MediaPlayerState, + type MediaStatus, + type RemoteMediaClient, +} from "react-native-google-cast"; +import type { useChromecastSegments } from "@/components/chromecast/hooks/useChromecastSegments"; +import { Text } from "@/components/common/Text"; + +type ChromecastSegments = ReturnType; + +interface CastPlayerPosterProps { + /** Poster image URL, or null when unavailable. */ + posterUrl: string | null; + /** Whether the cast media is currently buffering. */ + isBuffering: boolean; + /** The current playback segment (intro/credits/etc.), or null. */ + currentSegment: ChromecastSegments["currentSegment"]; + /** Skip the intro segment. */ + skipIntro: ChromecastSegments["skipIntro"]; + /** Skip the credits segment. */ + skipCredits: ChromecastSegments["skipCredits"]; + /** Skip the current generic segment. */ + skipSegment: ChromecastSegments["skipSegment"]; + /** The remote media client, or null when no session. */ + remoteMediaClient: RemoteMediaClient | null; + /** Raw Chromecast media status. */ + mediaStatus: MediaStatus | null; + /** Theme accent color. */ + protocolColor: string; + /** Translation function. */ + t: TFunction; +} + +export function CastPlayerPoster({ + posterUrl, + isBuffering, + currentSegment, + skipIntro, + skipCredits, + skipSegment, + remoteMediaClient, + mediaStatus, + protocolColor, + t, +}: CastPlayerPosterProps) { + return ( + + + {posterUrl ? ( + + ) : ( + + + + )} + + {/* Skip intro/credits bar at bottom of poster */} + {currentSegment && ( + { + if (!remoteMediaClient) return; + try { + const seekFn = async (positionMs: number) => { + if ( + mediaStatus?.playerState === MediaPlayerState.PLAYING || + mediaStatus?.playerState === MediaPlayerState.PAUSED + ) { + await remoteMediaClient.seek({ + position: positionMs / 1000, + }); + } + }; + if (currentSegment.type === "intro") { + await skipIntro(seekFn); + } else if (currentSegment.type === "credits") { + await skipCredits(seekFn); + } else { + await skipSegment(seekFn); + } + } catch (error) { + console.error("[Casting Player] Skip error:", error); + } + }} + style={{ + position: "absolute", + bottom: 0, + left: 0, + right: 0, + backgroundColor: protocolColor, + paddingVertical: 12, + paddingHorizontal: 16, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 8, + }} + > + + + {t( + `player.skip_${currentSegment.type === "credits" ? "outro" : currentSegment.type}`, + )} + + + )} + + {/* Buffering overlay */} + {isBuffering && ( + + + + {t("casting_player.buffering")} + + + )} + + + ); +}