diff --git a/app/(auth)/casting-player.tsx b/app/(auth)/casting-player.tsx index ac40646b2..dee974779 100644 --- a/app/(auth)/casting-player.tsx +++ b/app/(auth)/casting-player.tsx @@ -36,6 +36,7 @@ import Animated, { } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BITRATES } from "@/components/BitrateSelector"; +import { CastPlayerEpisodeControls } from "@/components/casting/player/CastPlayerEpisodeControls"; import { CastPlayerHeader } from "@/components/casting/player/CastPlayerHeader"; import { CastPlayerPoster } from "@/components/casting/player/CastPlayerPoster"; import { CastPlayerTitle } from "@/components/casting/player/CastPlayerTitle"; @@ -712,128 +713,16 @@ export default function CastingPlayerScreen() { {/* Fixed 4-button control row for episodes - positioned independently */} {currentItem.Type === "Episode" && ( - - {/* Episodes button */} - setShowEpisodeList(true)} - style={{ - flex: 1, - backgroundColor: "#1a1a1a", - padding: 12, - borderRadius: 12, - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - }} - > - - - - {/* Previous episode button */} - { - const currentIndex = episodes.findIndex( - (ep) => ep.Id === currentItem.Id, - ); - if (currentIndex > 0) { - await loadEpisode(episodes[currentIndex - 1]); - } - }} - disabled={ - episodes.findIndex((ep) => ep.Id === currentItem.Id) <= 0 - } - style={{ - flex: 1, - backgroundColor: "#1a1a1a", - padding: 12, - borderRadius: 12, - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - opacity: - episodes.findIndex((ep) => ep.Id === currentItem.Id) <= 0 - ? 0.4 - : 1, - }} - > - - - - {/* Next episode button */} - { - if (nextEpisode) { - await loadEpisode(nextEpisode); - } - }} - disabled={!nextEpisode} - style={{ - flex: 1, - backgroundColor: "#1a1a1a", - padding: 12, - borderRadius: 12, - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - opacity: nextEpisode ? 1 : 0.4, - }} - > - - - - {/* Stop playback button - stops media but stays connected to Chromecast */} - { - try { - // Stop the current media playback (don't disconnect from Chromecast) - if (remoteMediaClient) { - await remoteMediaClient.stop(); - } - - // Navigate back/close the player (mini player will disappear since no media is playing) - if (router.canGoBack()) { - router.back(); - } else { - router.replace("/(auth)/(tabs)/(home)/"); - } - } catch (error) { - console.error( - "[Casting Player] Error stopping playback:", - error, - ); - // Navigate anyway - if (router.canGoBack()) { - router.back(); - } else { - router.replace("/(auth)/(tabs)/(home)/"); - } - } - }} - style={{ - flex: 1, - backgroundColor: "#1a1a1a", - padding: 12, - borderRadius: 12, - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - }} - > - - - + setShowEpisodeList(true)} + loadEpisode={loadEpisode} + router={router} + /> )} {/* Fixed bottom controls area */} diff --git a/components/casting/player/CastPlayerEpisodeControls.tsx b/components/casting/player/CastPlayerEpisodeControls.tsx new file mode 100644 index 000000000..3ddebbc66 --- /dev/null +++ b/components/casting/player/CastPlayerEpisodeControls.tsx @@ -0,0 +1,158 @@ +/** + * Casting Player Episode Controls + * Fixed 4-button control row for episodes: episode list, previous, next, stop. + */ + +import { Ionicons } from "@expo/vector-icons"; +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import type { Router } from "expo-router"; +import { Pressable, View } from "react-native"; +import type { RemoteMediaClient } from "react-native-google-cast"; + +interface CastPlayerEpisodeControlsProps { + /** Bottom safe-area inset, used to offset the fixed control row. */ + insetBottom: number; + /** Id of the currently playing episode. */ + currentItemId: BaseItemDto["Id"]; + /** Full episode list for the series. */ + episodes: BaseItemDto[]; + /** Next episode in the list, or null if none. */ + nextEpisode: BaseItemDto | null; + /** Remote media client, or null when no session. */ + remoteMediaClient: RemoteMediaClient | null; + /** Open the episode list modal. */ + onPressEpisodes: () => void; + /** Load a different episode on the Chromecast. */ + loadEpisode: (episode: BaseItemDto) => Promise; + /** Expo Router instance for navigation on stop. */ + router: Router; +} + +export function CastPlayerEpisodeControls({ + insetBottom, + currentItemId, + episodes, + nextEpisode, + remoteMediaClient, + onPressEpisodes, + loadEpisode, + router, +}: CastPlayerEpisodeControlsProps) { + return ( + + {/* Episodes button */} + + + + + {/* Previous episode button */} + { + const currentIndex = episodes.findIndex( + (ep) => ep.Id === currentItemId, + ); + if (currentIndex > 0) { + await loadEpisode(episodes[currentIndex - 1]); + } + }} + disabled={episodes.findIndex((ep) => ep.Id === currentItemId) <= 0} + style={{ + flex: 1, + backgroundColor: "#1a1a1a", + padding: 12, + borderRadius: 12, + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + opacity: + episodes.findIndex((ep) => ep.Id === currentItemId) <= 0 ? 0.4 : 1, + }} + > + + + + {/* Next episode button */} + { + if (nextEpisode) { + await loadEpisode(nextEpisode); + } + }} + disabled={!nextEpisode} + style={{ + flex: 1, + backgroundColor: "#1a1a1a", + padding: 12, + borderRadius: 12, + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + opacity: nextEpisode ? 1 : 0.4, + }} + > + + + + {/* Stop playback button - stops media but stays connected to Chromecast */} + { + try { + // Stop the current media playback (don't disconnect from Chromecast) + if (remoteMediaClient) { + await remoteMediaClient.stop(); + } + + // Navigate back/close the player (mini player will disappear since no media is playing) + if (router.canGoBack()) { + router.back(); + } else { + router.replace("/(auth)/(tabs)/(home)/"); + } + } catch (error) { + console.error("[Casting Player] Error stopping playback:", error); + // Navigate anyway + if (router.canGoBack()) { + router.back(); + } else { + router.replace("/(auth)/(tabs)/(home)/"); + } + } + }} + style={{ + flex: 1, + backgroundColor: "#1a1a1a", + padding: 12, + borderRadius: 12, + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + }} + > + + + + ); +}