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",
+ }}
+ >
+
+
+
+ );
+}