diff --git a/app/_layout.tsx b/app/_layout.tsx
index 79278b70..7d86119f 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -10,6 +10,7 @@ import * as BackgroundTask from "expo-background-task";
import * as Device from "expo-device";
import { Platform } from "react-native";
import { GlobalModal } from "@/components/GlobalModal";
+
import i18n from "@/i18n";
import { DownloadProvider } from "@/providers/DownloadProvider";
import { GlobalModalProvider } from "@/providers/GlobalModalProvider";
@@ -443,7 +444,7 @@ function Layout() {
}}
closeButton
/>
-
+ {!Platform.isTV && }
diff --git a/components/ItemContent.tv.tsx b/components/ItemContent.tv.tsx
index 8286b595..f0c33cc6 100644
--- a/components/ItemContent.tv.tsx
+++ b/components/ItemContent.tv.tsx
@@ -25,6 +25,7 @@ import {
Pressable,
ScrollView,
TVFocusGuideView,
+ useTVEventHandler,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -301,6 +302,63 @@ const _TVOptionRowModal: React.FC<{
);
};
+// Cancel button for TV option selectors
+const TVCancelButton: React.FC<{ onPress: () => void }> = ({ onPress }) => {
+ const { t } = useTranslation();
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new Animated.Value(1)).current;
+
+ const animateTo = (v: number) =>
+ Animated.timing(scale, {
+ toValue: v,
+ duration: 120,
+ easing: Easing.out(Easing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ return (
+ {
+ setFocused(true);
+ animateTo(1.05);
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1);
+ }}
+ >
+
+
+
+ {t("common.cancel") || "Cancel"}
+
+
+
+ );
+};
+
// TV Option Selector - Bottom sheet with horizontal scrolling (Apple TV style)
const TVOptionSelector = ({
visible,
@@ -452,6 +510,19 @@ const TVOptionSelector = ({
))}
)}
+
+ {/* Cancel button */}
+ {isReady && (
+
+
+
+ )}
@@ -940,6 +1011,15 @@ export const ItemContentTV: React.FC = React.memo(
}
}, [isModalOpen]);
+ // tvOS menu button handler for closing modals
+ // Note: This may not receive events if React Navigation intercepts them first
+ useTVEventHandler((evt) => {
+ if (!evt || !isModalOpen) return;
+ if (evt.eventType === "menu" || evt.eventType === "back") {
+ setOpenModal(null);
+ }
+ });
+
// Get available audio tracks
const audioTracks = useMemo(() => {
const streams = selectedOptions?.mediaSource?.MediaStreams?.filter(
diff --git a/components/video-player/controls/Controls.tv.tsx b/components/video-player/controls/Controls.tv.tsx
index b421d7db..b9022f62 100644
--- a/components/video-player/controls/Controls.tv.tsx
+++ b/components/video-player/controls/Controls.tv.tsx
@@ -212,6 +212,13 @@ const TVOptionSelector = ({
))}
)}
+
+ {/* Cancel button */}
+ {isReady && (
+
+
+
+ )}
@@ -219,6 +226,61 @@ const TVOptionSelector = ({
);
};
+// Cancel button for TV option selectors
+const TVCancelButton: React.FC<{ onPress: () => void; label: string }> = ({
+ onPress,
+ label,
+}) => {
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new RNAnimated.Value(1)).current;
+
+ const animateTo = (v: number) =>
+ RNAnimated.timing(scale, {
+ toValue: v,
+ duration: 120,
+ easing: RNEasing.out(RNEasing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ return (
+ {
+ setFocused(true);
+ animateTo(1.05);
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1);
+ }}
+ >
+
+
+
+ {label}
+
+
+
+ );
+};
+
// Option card for horizontal selector (with forwardRef for programmatic focus)
const TVOptionCard = React.forwardRef<
View,
@@ -663,6 +725,23 @@ const selectorStyles = StyleSheet.create({
tabText: {
fontSize: 18,
},
+ cancelButtonContainer: {
+ paddingHorizontal: 48,
+ paddingTop: 16,
+ alignItems: "flex-start",
+ },
+ cancelButton: {
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: 12,
+ paddingVertical: 12,
+ paddingHorizontal: 20,
+ gap: 8,
+ },
+ cancelButtonText: {
+ fontSize: 16,
+ fontWeight: "600",
+ },
});
// TV Next Episode Countdown component - horizontal layout with animated progress bar
diff --git a/components/video-player/controls/TVSubtitleSheet.tsx b/components/video-player/controls/TVSubtitleSheet.tsx
index 8d0d22a9..7c393267 100644
--- a/components/video-player/controls/TVSubtitleSheet.tsx
+++ b/components/video-player/controls/TVSubtitleSheet.tsx
@@ -474,6 +474,61 @@ const SubtitleResultCard = React.forwardRef<
);
});
+// Cancel button for TV subtitle sheet
+const TVCancelButton: React.FC<{ onPress: () => void; label: string }> = ({
+ onPress,
+ label,
+}) => {
+ const [focused, setFocused] = useState(false);
+ const scale = useRef(new RNAnimated.Value(1)).current;
+
+ const animateTo = (v: number) =>
+ RNAnimated.timing(scale, {
+ toValue: v,
+ duration: 120,
+ easing: RNEasing.out(RNEasing.quad),
+ useNativeDriver: true,
+ }).start();
+
+ return (
+ {
+ setFocused(true);
+ animateTo(1.05);
+ }}
+ onBlur={() => {
+ setFocused(false);
+ animateTo(1);
+ }}
+ >
+
+
+
+ {label}
+
+
+
+ );
+};
+
export const TVSubtitleSheet: React.FC = ({
visible,
item,
@@ -847,6 +902,16 @@ export const TVSubtitleSheet: React.FC = ({
)}
>
)}
+
+ {/* Cancel button */}
+ {isReady && (
+
+
+
+ )}
@@ -1086,4 +1151,21 @@ const styles = StyleSheet.create({
color: "rgba(255,255,255,0.4)",
fontSize: 12,
},
+ cancelButtonContainer: {
+ paddingHorizontal: 48,
+ paddingTop: 20,
+ alignItems: "flex-start",
+ },
+ cancelButton: {
+ flexDirection: "row",
+ alignItems: "center",
+ borderRadius: 12,
+ paddingVertical: 12,
+ paddingHorizontal: 20,
+ gap: 8,
+ },
+ cancelButtonText: {
+ fontSize: 16,
+ fontWeight: "600",
+ },
});