diff --git a/src/components/CustomAlert.tsx b/src/components/CustomAlert.tsx
index 14fb993..d88e341 100644
--- a/src/components/CustomAlert.tsx
+++ b/src/components/CustomAlert.tsx
@@ -5,7 +5,6 @@ import {
Text,
StyleSheet,
Pressable,
- TouchableOpacity,
useColorScheme,
Platform,
} from 'react-native';
@@ -16,6 +15,7 @@ import Animated, {
} from 'react-native-reanimated';
import { useTheme } from '../contexts/ThemeContext';
import { Portal } from 'react-native-paper';
+import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
interface CustomAlertProps {
visible: boolean;
@@ -120,7 +120,7 @@ export const CustomAlert = ({
{actions.map((action, idx) => {
const isPrimary = idx === actions.length - 1;
return (
- handleActionPress(action)}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && visible && isPrimary}
>
{action.label}
-
+
);
})}
diff --git a/src/components/ProviderFilter.tsx b/src/components/ProviderFilter.tsx
index 89005d9..ba73cc3 100644
--- a/src/components/ProviderFilter.tsx
+++ b/src/components/ProviderFilter.tsx
@@ -1,5 +1,6 @@
-import React, { memo, useCallback } from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, FlatList } from 'react-native';
+import React, { memo, useCallback, useRef } from 'react';
+import { View, Text, StyleSheet, FlatList, Platform } from 'react-native';
+import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
interface ProviderFilterProps {
selectedProvider: string;
@@ -15,14 +16,24 @@ const ProviderFilter = memo(({
theme
}: ProviderFilterProps) => {
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
+ const listRef = useRef | null>(null);
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
- onSelect(item.id)}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={16}
+ onFocus={() => {
+ if (!Platform.isTV) return;
+ try {
+ listRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0.5 });
+ } catch { }
+ }}
>
{item.name}
-
+
), [selectedProvider, onSelect, styles]);
return (
item.id}
diff --git a/src/components/StreamCard.tsx b/src/components/StreamCard.tsx
index 2040c4f..88200c3 100644
--- a/src/components/StreamCard.tsx
+++ b/src/components/StreamCard.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
ActivityIndicator,
Platform,
Clipboard,
@@ -16,6 +15,7 @@ import QualityBadge from './metadata/QualityBadge';
import { useSettings } from '../hooks/useSettings';
import { useDownloads } from '../contexts/DownloadsContext';
import { useToast } from '../contexts/ToastContext';
+import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
interface StreamCardProps {
stream: Stream;
@@ -177,16 +177,22 @@ const StreamCard = memo(({
const isDebrid = streamInfo.isDebrid;
return (
-
+
{/* Scraper Logo */}
{showLogos && scraperLogo && (
@@ -250,21 +256,23 @@ const StreamCard = memo(({
-
- {settings?.enableDownloads !== false && (
-
+
+ {settings?.enableDownloads !== false && (
+
+
-
-
- )}
-
+
+
+
+ )}
+
);
});
diff --git a/src/components/common/FocusablePressable.tsx b/src/components/common/FocusablePressable.tsx
new file mode 100644
index 0000000..901aa00
--- /dev/null
+++ b/src/components/common/FocusablePressable.tsx
@@ -0,0 +1,171 @@
+import React, { useCallback, useMemo, useRef, useState } from 'react';
+import {
+ Animated,
+ Easing,
+ Platform,
+ Pressable,
+ PressableProps,
+ StyleProp,
+ StyleSheet,
+ View,
+ ViewStyle,
+} from 'react-native';
+import { useTheme } from '../../contexts/ThemeContext';
+import { tvFocusPresets, TVFocusPresetName } from '../../styles/tvFocus';
+
+export type FocusablePressableProps = Omit & {
+ containerStyle?: StyleProp;
+ style?: StyleProp | ((state: { pressed: boolean }) => StyleProp);
+ preset?: TVFocusPresetName;
+ enableTVFocus?: boolean;
+ focusBorderRadius?: number;
+ focusScale?: number;
+ focusRingWidth?: number;
+ focusRingColor?: string;
+};
+
+export const FocusablePressable: React.FC = ({
+ enableTVFocus = Platform.isTV,
+ containerStyle,
+ style,
+ preset,
+ focusBorderRadius,
+ focusScale,
+ focusRingWidth,
+ focusRingColor,
+ onFocus,
+ onBlur,
+ children,
+ ...rest
+}) => {
+ const { currentTheme } = useTheme();
+ const ringColor = focusRingColor ?? currentTheme.colors.primary;
+
+ const resolvedPreset = preset ? tvFocusPresets[preset] : undefined;
+ const resolvedBorderRadius = focusBorderRadius ?? resolvedPreset?.focusBorderRadius ?? 12;
+ const resolvedScale = focusScale ?? resolvedPreset?.focusScale ?? 1.06;
+ const resolvedRingWidth = focusRingWidth ?? resolvedPreset?.focusRingWidth ?? 3;
+
+ const focusProgress = useRef(new Animated.Value(0)).current;
+ const [isFocused, setIsFocused] = useState(false);
+
+ const animateTo = useCallback((toValue: 0 | 1) => {
+ Animated.timing(focusProgress, {
+ toValue,
+ duration: 140,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }).start();
+ }, [focusProgress]);
+
+ const handleFocus = useCallback((e: any) => {
+ if (enableTVFocus) {
+ setIsFocused(true);
+ animateTo(1);
+ }
+ onFocus?.(e);
+ }, [enableTVFocus, animateTo, onFocus]);
+
+ const handleBlur = useCallback((e: any) => {
+ if (enableTVFocus) {
+ setIsFocused(false);
+ animateTo(0);
+ }
+ onBlur?.(e);
+ }, [enableTVFocus, animateTo, onBlur]);
+
+ const containerAnimatedStyle = useMemo(() => {
+ if (!enableTVFocus) return null;
+ return {
+ transform: [
+ {
+ scale: focusProgress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [1, resolvedScale],
+ }),
+ },
+ ],
+ } as any;
+ }, [enableTVFocus, focusProgress, resolvedScale]);
+
+ const ringAnimatedStyle = useMemo(() => {
+ if (!enableTVFocus) return null;
+ return {
+ opacity: focusProgress,
+ transform: [
+ {
+ scale: focusProgress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0.985, 1],
+ }),
+ },
+ ],
+ } as any;
+ }, [enableTVFocus, focusProgress]);
+
+ return (
+
+ {
+ const base = typeof style === 'function' ? style({ pressed: state.pressed }) : style;
+ return [base, { position: 'relative' } as ViewStyle] as any;
+ }}
+ >
+ {children}
+ {enableTVFocus && (
+ <>
+
+
+ >
+ )}
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ focusRing: {
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ },
+});
+
diff --git a/src/components/common/FocusableTouchableOpacity.tsx b/src/components/common/FocusableTouchableOpacity.tsx
new file mode 100644
index 0000000..97c7e80
--- /dev/null
+++ b/src/components/common/FocusableTouchableOpacity.tsx
@@ -0,0 +1,192 @@
+import React, { useCallback, useMemo, useRef, useState } from 'react';
+import {
+ Animated,
+ Easing,
+ Platform,
+ StyleProp,
+ StyleSheet,
+ TouchableOpacity,
+ TouchableOpacityProps,
+ View,
+ ViewStyle,
+} from 'react-native';
+import { useTheme } from '../../contexts/ThemeContext';
+import { tvFocusPresets, TVFocusPresetName } from '../../styles/tvFocus';
+
+export type FocusableTouchableOpacityProps = Omit & {
+ /**
+ * Optional style applied to the outer Animated wrapper.
+ * Useful when the touchable itself is absolutely positioned (e.g. overlays).
+ */
+ containerStyle?: StyleProp;
+ style?: StyleProp;
+ /** Optional preset to standardize focus behavior across the app. */
+ preset?: TVFocusPresetName;
+ /**
+ * When true, focus visuals are enabled (defaults to Platform.isTV).
+ * You can force-enable for large-screen non-TV if desired.
+ */
+ enableTVFocus?: boolean;
+ /** Border radius for the focus ring overlay. */
+ focusBorderRadius?: number;
+ /** Scale applied when focused. */
+ focusScale?: number;
+ /** Focus ring thickness (overlay, doesn't affect layout). */
+ focusRingWidth?: number;
+ /** Focus ring color (defaults to theme primary). */
+ focusRingColor?: string;
+};
+
+export const FocusableTouchableOpacity: React.FC = ({
+ enableTVFocus = Platform.isTV,
+ containerStyle,
+ preset,
+ focusBorderRadius,
+ focusScale,
+ focusRingWidth,
+ focusRingColor,
+ onFocus,
+ onBlur,
+ activeOpacity,
+ style,
+ children,
+ ...rest
+}) => {
+ const { currentTheme } = useTheme();
+ const ringColor = focusRingColor ?? currentTheme.colors.primary;
+
+ const resolvedPreset = preset ? tvFocusPresets[preset] : undefined;
+ const resolvedBorderRadius = focusBorderRadius ?? resolvedPreset?.focusBorderRadius ?? 12;
+ const resolvedScale = focusScale ?? resolvedPreset?.focusScale ?? 1.06;
+ const resolvedRingWidth = focusRingWidth ?? resolvedPreset?.focusRingWidth ?? 3;
+
+ const focusProgress = useRef(new Animated.Value(0)).current;
+ const [isFocused, setIsFocused] = useState(false);
+
+ const animateTo = useCallback((toValue: 0 | 1) => {
+ Animated.timing(focusProgress, {
+ toValue,
+ duration: 140,
+ easing: Easing.out(Easing.cubic),
+ useNativeDriver: true,
+ }).start();
+ }, [focusProgress]);
+
+ const handleFocus = useCallback((e: any) => {
+ if (enableTVFocus) {
+ setIsFocused(true);
+ animateTo(1);
+ }
+ onFocus?.(e);
+ }, [enableTVFocus, animateTo, onFocus]);
+
+ const handleBlur = useCallback((e: any) => {
+ if (enableTVFocus) {
+ setIsFocused(false);
+ animateTo(0);
+ }
+ onBlur?.(e);
+ }, [enableTVFocus, animateTo, onBlur]);
+
+ const containerAnimatedStyle = useMemo(() => {
+ if (!enableTVFocus) return null;
+ return {
+ transform: [
+ {
+ scale: focusProgress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [1, resolvedScale],
+ }),
+ },
+ ],
+ } as any;
+ }, [enableTVFocus, focusProgress, resolvedScale]);
+
+ const ringAnimatedStyle = useMemo(() => {
+ if (!enableTVFocus) return null;
+ return {
+ opacity: focusProgress,
+ transform: [
+ {
+ scale: focusProgress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0.985, 1],
+ }),
+ },
+ ],
+ } as any;
+ }, [enableTVFocus, focusProgress]);
+
+ // Avoid the default "dim" feel on TV by not changing opacity on press.
+ const finalActiveOpacity = enableTVFocus ? 1 : (activeOpacity ?? 0.7);
+
+ if (!enableTVFocus) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return (
+
+
+ {children}
+
+ {/* Slight inner highlight to make focus readable on very bright posters */}
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ focusRing: {
+ // Keep ring inside bounds to avoid overflow clipping.
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ },
+});
+
diff --git a/src/components/common/ScreenHeader.tsx b/src/components/common/ScreenHeader.tsx
index 2ab1a4f..7dac1b9 100644
--- a/src/components/common/ScreenHeader.tsx
+++ b/src/components/common/ScreenHeader.tsx
@@ -3,13 +3,13 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
StatusBar,
Platform,
} from 'react-native';
import { useTheme } from '../../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Feather, MaterialIcons } from '@expo/vector-icons';
+import { FocusableTouchableOpacity } from './FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@@ -131,17 +131,20 @@ const ScreenHeader: React.FC = ({
>
{showBackButton ? (
-
-
+
) : null}
{titleComponent ? (
@@ -164,17 +167,20 @@ const ScreenHeader: React.FC = ({
{rightActionComponent ? (
{rightActionComponent}
) : rightActionIcon && onRightActionPress ? (
-
-
+
) : (
)}
diff --git a/src/components/home/AppleTVHero.tsx b/src/components/home/AppleTVHero.tsx
index d801a95..31446c7 100644
--- a/src/components/home/AppleTVHero.tsx
+++ b/src/components/home/AppleTVHero.tsx
@@ -45,6 +45,7 @@ import { useTraktContext } from '../../contexts/TraktContext';
import { BlurView as ExpoBlurView } from 'expo-blur';
import { useWatchProgress } from '../../hooks/useWatchProgress';
import { streamCacheService } from '../../services/streamCacheService';
+import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
interface AppleTVHeroProps {
featuredContent: StreamingContent | null;
@@ -1177,7 +1178,7 @@ const AppleTVHero: React.FC = ({
style={logoAnimatedStyle}
>
{currentItem.logo && !logoError[currentIndex] ? (
- {
if (currentItem) {
@@ -1188,6 +1189,10 @@ const AppleTVHero: React.FC = ({
}
}}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={16}
+ focusRingWidth={3}
+ focusScale={1.03}
>
= ({
}}
/>
-
+
) : (
- {
if (currentItem) {
@@ -1224,13 +1229,17 @@ const AppleTVHero: React.FC = ({
}
}}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={16}
+ focusRingWidth={3}
+ focusScale={1.03}
>
{currentItem.name}
-
+
)}
@@ -1253,12 +1262,16 @@ const AppleTVHero: React.FC = ({
{/* Action Buttons - Play and Save buttons */}
{/* Play Button */}
-
= ({
color="#000"
/>
{playButtonText}
-
+
{/* Save Button */}
-
-
+
{/* Pagination Dots */}
diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx
index a7e4e60..b7c7adf 100644
--- a/src/components/home/ContentItem.tsx
+++ b/src/components/home/ContentItem.tsx
@@ -13,6 +13,7 @@ import { storageService } from '../../services/storageService';
import { TraktService } from '../../services/traktService';
import { useTraktContext } from '../../contexts/TraktContext';
import Animated, { FadeIn } from 'react-native-reanimated';
+import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
interface ContentItemProps {
item: StreamingContent;
@@ -302,12 +303,18 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
return (
<>
-
{/* Image with FastImage for aggressive caching */}
@@ -362,7 +369,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
)}
-
+
{settings.showPosterTitles && (
((props, re
// Memoized render function for continue watching items
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
- ((props, re
onPress={() => handleContentPress(item)}
onLongPress={() => handleLongPress(item)}
delayLongPress={800}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={14}
+ focusRingColor={currentTheme.colors.primary}
+ focusRingWidth={3}
+ focusScale={isTV ? 1.06 : 1.04}
>
{/* Poster Image */}
((props, re
)}
-
+
), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet]);
// Memoized key extractor
diff --git a/src/components/home/DropUpMenu.tsx b/src/components/home/DropUpMenu.tsx
index 41054ea..796f619 100644
--- a/src/components/home/DropUpMenu.tsx
+++ b/src/components/home/DropUpMenu.tsx
@@ -5,7 +5,6 @@ import {
StyleSheet,
Modal,
Pressable,
- TouchableOpacity,
useColorScheme,
Dimensions,
Platform
@@ -28,6 +27,7 @@ import {
GestureHandlerRootView,
} from 'react-native-gesture-handler';
import { StreamingContent } from '../../services/catalogService';
+import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
interface DropUpMenuProps {
visible: boolean;
@@ -184,7 +184,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
{menuOptions.map((option, index) => (
-
{option.label}
-
+
))}
diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx
index fd2751b..825fcb4 100644
--- a/src/components/home/FeaturedContent.tsx
+++ b/src/components/home/FeaturedContent.tsx
@@ -30,6 +30,7 @@ import { SkeletonFeatured } from './SkeletonLoaders';
import { hasValidLogoFormat, isTmdbUrl } from '../../utils/logoUtils';
import { logger } from '../../utils/logger';
import { useTheme } from '../../contexts/ThemeContext';
+import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
interface FeaturedContentProps {
featuredContent: StreamingContent | null;
@@ -495,7 +496,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
)}
- {
if (featuredContent) {
@@ -507,12 +508,17 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
}}
activeOpacity={0.8}
hasTVPreferredFocus={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={30}
+ focusRingColor={currentTheme.colors.primary}
+ focusRingWidth={3}
+ focusScale={1.04}
>
Play Now
-
+
- {
if (featuredContent) {
@@ -643,12 +649,17 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
}}
activeOpacity={0.8}
hasTVPreferredFocus={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={30}
+ focusRingColor={currentTheme.colors.primary}
+ focusRingWidth={3}
+ focusScale={1.04}
>
Play
-
+
{/* Single Row Layout - Play, Save, and optionally Collection/Ratings */}
-
{
@@ -366,9 +371,9 @@ const ActionButtons = memo(({
color={isWatched && type === 'movie' ? "#fff" : "#000"}
/>
{finalPlayButtonText}
-
+
-
{Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? (
@@ -399,15 +408,19 @@ const ActionButtons = memo(({
{inLibrary ? 'Saved' : 'Save'}
-
+
{/* Trakt Collection Button */}
{hasTraktCollection && (
-
{Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? (
@@ -426,16 +439,20 @@ const ActionButtons = memo(({
size={isTablet ? 28 : 24}
color={isInCollection ? "#3498DB" : currentTheme.colors.white}
/>
-
+
)}
{/* Ratings Button (for series) */}
{hasRatings && (
-
{Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? (
@@ -454,7 +471,7 @@ const ActionButtons = memo(({
size={isTablet ? 28 : 24}
color={currentTheme.colors.white}
/>
-
+
)}
@@ -1757,7 +1774,7 @@ const HeroSection: React.FC = memo(({
right: width >= 768 ? 32 : 16,
zIndex: 1000,
}}>
- {
// Extract episode info if it's a series
let episodeData = null;
@@ -1782,6 +1799,10 @@ const HeroSection: React.FC = memo(({
}}
activeOpacity={0.7}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={20}
+ focusRingWidth={3}
+ focusScale={1.06}
style={{
padding: 8,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
@@ -1793,19 +1814,27 @@ const HeroSection: React.FC = memo(({
size={24}
color="white"
/>
-
+
)}
-
+
-
+
{/* Ultra-light Gradient with subtle dynamic background blend */}
diff --git a/src/components/metadata/MoreLikeThisSection.tsx b/src/components/metadata/MoreLikeThisSection.tsx
index 3fca29b..edc5ed8 100644
--- a/src/components/metadata/MoreLikeThisSection.tsx
+++ b/src/components/metadata/MoreLikeThisSection.tsx
@@ -18,6 +18,7 @@ import { useTheme } from '../../contexts/ThemeContext';
import { TMDBService } from '../../services/tmdbService';
import { catalogService } from '../../services/catalogService';
import CustomAlert from '../../components/CustomAlert';
+import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
const { width } = Dimensions.get('window');
@@ -117,9 +118,15 @@ export const MoreLikeThisSection: React.FC = ({
};
const renderItem = ({ item }: { item: StreamingContent }) => (
- handleItemPress(item)}
+ activeOpacity={0.9}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={isTV ? 12 : isLargeTablet ? 10 : isTablet ? 10 : 8}
+ focusRingColor={currentTheme.colors.primary}
+ focusRingWidth={3}
+ focusScale={isTV ? 1.06 : 1.04}
>
= ({
{item.name}
-
+
);
if (loadingRecommendations) {
diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx
index 5be5fb7..43af294 100644
--- a/src/components/metadata/SeriesContent.tsx
+++ b/src/components/metadata/SeriesContent.tsx
@@ -16,6 +16,7 @@ import { TraktService } from '../../services/traktService';
import { watchedService } from '../../services/watchedService';
import { logger } from '../../utils/logger';
import { mmkvStorage } from '../../services/mmkvStorage';
+import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
// Enhanced responsive breakpoints for Seasons Section
const BREAKPOINTS = {
@@ -779,7 +780,7 @@ const SeriesContentComponent: React.FC = ({
]}>Seasons
{/* Dropdown Toggle Button */}
- = ({
]}
activeOpacity={0.7}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={isTV ? 10 : isLargeTablet ? 8 : isTablet ? 6 : 6}
+ focusRingWidth={3}
+ focusScale={1.03}
>
= ({
]}>
{seasonViewMode === 'posters' ? 'Posters' : 'Text'}
-
+
= ({
key={season}
style={{ opacity: textViewVisible ? 1 : 0 }}
>
- = ({
]}
onPress={() => onSeasonChange(season)}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12}
+ focusRingWidth={3}
+ focusScale={1.03}
>
= ({
]} numberOfLines={1}>
{season === 0 ? 'Specials' : `Season ${season}`}
-
+
);
}
@@ -885,7 +894,7 @@ const SeriesContentComponent: React.FC = ({
key={season}
style={{ opacity: posterViewVisible ? 1 : 0 }}
>
- = ({
]}
onPress={() => onSeasonChange(season)}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8}
+ focusRingWidth={3}
+ focusScale={1.03}
>
= ({
>
{season === 0 ? 'Specials' : `Season ${season}`}
-
+
);
}}
@@ -1022,7 +1035,7 @@ const SeriesContentComponent: React.FC = ({
const showProgress = progress && progressPercent < 85;
return (
- = ({
delayLongPress={400}
activeOpacity={0.7}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
+ focusRingWidth={3}
+ focusScale={1.02}
>
= ({
{(episode.overview || (episode as any).description || (episode as any).plot || (episode as any).synopsis || 'No description available')}
-
+
);
};
diff --git a/src/components/metadata/TrailersSection.tsx b/src/components/metadata/TrailersSection.tsx
index 536e3c9..0a5ff63 100644
--- a/src/components/metadata/TrailersSection.tsx
+++ b/src/components/metadata/TrailersSection.tsx
@@ -20,6 +20,7 @@ import { logger } from '../../utils/logger';
import TrailerService from '../../services/trailerService';
import TrailerModal from './TrailerModal';
import Animated, { useSharedValue, withTiming, withDelay, useAnimatedStyle } from 'react-native-reanimated';
+import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
// Enhanced responsive breakpoints for Trailers Section
const BREAKPOINTS = {
@@ -517,7 +518,7 @@ const TrailersSection: React.FC = memo(({
{/* Category Selector - Right Aligned */}
{trailerCategories.length > 0 && selectedCategory && (
- = memo(({
onPress={toggleDropdown}
activeOpacity={0.8}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
+ focusRingWidth={3}
+ focusScale={1.03}
>
= memo(({
size={isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18}
color="rgba(255,255,255,0.7)"
/>
-
+
)}
@@ -575,7 +580,7 @@ const TrailersSection: React.FC = memo(({
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
}]}>
{trailerCategories.map(category => (
- = memo(({
onPress={() => handleCategorySelect(category)}
activeOpacity={0.7}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={isTV ? 18 : isLargeTablet ? 16 : isTablet ? 14 : 14}
+ focusRingWidth={3}
+ focusScale={1.02}
>
= memo(({
{trailers[category].length}
-
+
))}
@@ -656,7 +665,7 @@ const TrailersSection: React.FC = memo(({
{ width: trailerCardWidth }
]}
>
- = memo(({
onPress={() => handleTrailerPress(trailer)}
activeOpacity={0.9}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
+ focusRingWidth={3}
+ focusScale={1.03}
>
{/* Thumbnail with Gradient Overlay */}
@@ -688,7 +701,7 @@ const TrailersSection: React.FC = memo(({
}
]} />
-
+
{/* Trailer Info Below Card */}
diff --git a/src/components/player/modals/AudioTrackModal.tsx b/src/components/player/modals/AudioTrackModal.tsx
index 07e61a0..1424910 100644
--- a/src/components/player/modals/AudioTrackModal.tsx
+++ b/src/components/player/modals/AudioTrackModal.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native';
+import { View, Text, Pressable, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
@@ -9,6 +9,7 @@ import Animated, {
} from 'react-native-reanimated';
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
import { logger } from '../../../utils/logger';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface AudioTrackModalProps {
showAudioModal: boolean;
@@ -38,17 +39,17 @@ export const AudioTrackModal: React.FC = ({
return (
{/* Backdrop matching SubtitleModal */}
-
-
+
{/* Center Alignment Container */}
@@ -79,7 +80,7 @@ export const AudioTrackModal: React.FC = ({
const isSelected = selectedAudioTrack === track.id;
return (
- {
selectAudioTrack(track.id);
@@ -93,6 +94,10 @@ export const AudioTrackModal: React.FC = ({
justifyContent: 'space-between',
alignItems: 'center'
}}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && isSelected}
>
= ({
{isSelected && }
-
+
);
})}
diff --git a/src/components/player/modals/EpisodeStreamsModal.tsx b/src/components/player/modals/EpisodeStreamsModal.tsx
index 935d976..aedf7dd 100644
--- a/src/components/player/modals/EpisodeStreamsModal.tsx
+++ b/src/components/player/modals/EpisodeStreamsModal.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native';
+import { View, Text, Pressable, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
@@ -11,6 +11,7 @@ import { Episode } from '../../../types/metadata';
import { Stream } from '../../../types/streams';
import { stremioService } from '../../../services/stremioService';
import { logger } from '../../../utils/logger';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface EpisodeStreamsModalProps {
visible: boolean;
@@ -142,17 +143,17 @@ export const EpisodeStreamsModal: React.FC = ({
return (
{/* Backdrop */}
-
-
+
= ({
const quality = getQualityFromTitle(stream.title) || stream.quality;
return (
- = ({
onClose();
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && index === 0}
>
@@ -248,7 +253,7 @@ export const EpisodeStreamsModal: React.FC = ({
)}
-
+
);
})}
diff --git a/src/components/player/modals/EpisodesModal.tsx b/src/components/player/modals/EpisodesModal.tsx
index 81dba41..4161d69 100644
--- a/src/components/player/modals/EpisodesModal.tsx
+++ b/src/components/player/modals/EpisodesModal.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform, ActivityIndicator } from 'react-native';
+import { View, Text, Pressable, ScrollView, useWindowDimensions, StyleSheet, Platform, ActivityIndicator } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
@@ -12,6 +12,7 @@ import { EpisodeCard } from '../cards/EpisodeCard';
import { storageService } from '../../../services/storageService';
import { TraktService } from '../../../services/traktService';
import { logger } from '../../../utils/logger';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface EpisodesModalProps {
showEpisodesModal: boolean;
@@ -97,9 +98,9 @@ export const EpisodesModal: React.FC = ({
return (
- setShowEpisodesModal(false)}>
+ setShowEpisodesModal(false)} focusable={false}>
-
+
= ({
if (b === 0) return -1;
return a - b;
}).map((season) => (
- setSelectedSeason(season)}
style={{
@@ -138,6 +139,10 @@ export const EpisodesModal: React.FC = ({
borderWidth: 1,
borderColor: selectedSeason === season ? 'white' : 'rgba(255,255,255,0.1)',
}}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={20}
+ hasTVPreferredFocus={Platform.isTV && selectedSeason === season}
>
= ({
}}>
{season === 0 ? 'Specials' : `Season ${season}`}
-
+
))}
diff --git a/src/components/player/modals/ErrorModal.tsx b/src/components/player/modals/ErrorModal.tsx
index 5d5cbf4..cbd7123 100644
--- a/src/components/player/modals/ErrorModal.tsx
+++ b/src/components/player/modals/ErrorModal.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { View, Text, TouchableOpacity, StyleSheet, useWindowDimensions, Platform } from 'react-native';
+import { View, Text, Pressable, StyleSheet, useWindowDimensions, Platform } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
@@ -7,6 +7,7 @@ import Animated, {
ZoomIn,
ZoomOut,
} from 'react-native-reanimated';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
// Check if running on TV platform
const isTV = Platform.isTV;
@@ -58,9 +59,9 @@ export const ErrorModal: React.FC = ({
return (
-
+
-
+
= ({
{errorDetails || 'An unknown error occurred during playback.'}
-
-
-
- {copied ? 'Copied to clipboard' : 'Copy error details'}
-
-
+ {!!ExpoClipboard && (
+
+
+
+ {copied ? 'Copied to clipboard' : 'Copy error details'}
+
+
+ )}
- = ({
}}
onPress={handleClose}
activeOpacity={0.9}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV}
>
= ({
}}>
Dismiss
-
+
);
diff --git a/src/components/player/modals/LoadingOverlay.tsx b/src/components/player/modals/LoadingOverlay.tsx
index 0e5710c..fb9b3e9 100644
--- a/src/components/player/modals/LoadingOverlay.tsx
+++ b/src/components/player/modals/LoadingOverlay.tsx
@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
-import { View, TouchableOpacity, Animated, ActivityIndicator, StyleSheet, Image } from 'react-native';
+import { View, Animated, ActivityIndicator, StyleSheet, Image, Platform } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import Reanimated, {
@@ -12,6 +12,7 @@ import Reanimated, {
withDelay
} from 'react-native-reanimated';
import { styles } from '../utils/playerStyles';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface LoadingOverlayProps {
visible: boolean;
@@ -118,13 +119,17 @@ const LoadingOverlay: React.FC = ({
style={StyleSheet.absoluteFill}
/>
-
-
+
{hasLogo && logo ? (
diff --git a/src/components/player/modals/ResumeOverlay.tsx b/src/components/player/modals/ResumeOverlay.tsx
index a5ff183..7014d10 100644
--- a/src/components/player/modals/ResumeOverlay.tsx
+++ b/src/components/player/modals/ResumeOverlay.tsx
@@ -1,10 +1,11 @@
import React, { useEffect } from 'react';
-import { View, Text, TouchableOpacity } from 'react-native';
+import { View, Text, Platform } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { styles } from '../utils/playerStyles';
import { formatTime } from '../utils/playerUtils';
import { logger } from '../../../utils/logger';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface ResumeOverlayProps {
showResumeOverlay: boolean;
@@ -71,20 +72,27 @@ export const ResumeOverlay: React.FC = ({
-
Start Over
-
-
+
Resume
-
+
diff --git a/src/components/player/modals/SourcesModal.tsx b/src/components/player/modals/SourcesModal.tsx
index b126a5e..c4abc0b 100644
--- a/src/components/player/modals/SourcesModal.tsx
+++ b/src/components/player/modals/SourcesModal.tsx
@@ -8,6 +8,7 @@ import Animated, {
SlideOutRight,
} from 'react-native-reanimated';
import { Stream } from '../../../types/streams';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface SourcesModalProps {
showSourcesModal: boolean;
@@ -168,7 +169,7 @@ export const SourcesModal: React.FC = ({
const quality = getQualityFromTitle(stream.title) || stream.quality;
return (
- = ({
onPress={() => handleStreamSelect(stream)}
activeOpacity={0.7}
disabled={isChangingSource === true}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && (isSelected || (providerId === sortedProviders[0]?.[0] && index === 0))}
>
@@ -227,7 +232,7 @@ export const SourcesModal: React.FC = ({
)}
-
+
);
})}
diff --git a/src/components/player/modals/SpeedModal.tsx b/src/components/player/modals/SpeedModal.tsx
index 4e15803..dc6882c 100644
--- a/src/components/player/modals/SpeedModal.tsx
+++ b/src/components/player/modals/SpeedModal.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { View, Text, TouchableOpacity, useWindowDimensions, StyleSheet } from 'react-native';
+import { View, Text, Pressable, useWindowDimensions, StyleSheet, Platform } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
@@ -9,6 +9,7 @@ import Animated, {
useAnimatedStyle,
withTiming,
} from 'react-native-reanimated';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface SpeedModalProps {
showSpeedModal: boolean;
@@ -31,7 +32,15 @@ const MorphingButton = ({ label, isSelected, onPress, isSmall = false }: any) =>
});
return (
-
+
{label}
-
+
);
};
@@ -62,14 +71,14 @@ const SpeedModal: React.FC = ({
if (!showSpeedModal) return null;
return (
-
-
+ setShowSpeedModal(false)}
+ focusable={false}
>
-
+
= ({
{/* On Hold Section */}
- setHoldToSpeedEnabled(!holdToSpeedEnabled)}
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: holdToSpeedEnabled ? 15 : 0 }}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV}
>
On Hold
= ({
}}>
-
+
{holdToSpeedEnabled && (
diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx
index b1491d0..7255dfa 100644
--- a/src/components/player/modals/SubtitleModals.tsx
+++ b/src/components/player/modals/SubtitleModals.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { View, Text, TouchableOpacity, ScrollView, Platform, useWindowDimensions, StyleSheet } from 'react-native';
+import { View, Text, Pressable, ScrollView, Platform, useWindowDimensions, StyleSheet } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import Animated, {
FadeIn,
@@ -11,6 +11,7 @@ import Animated, {
} from 'react-native-reanimated';
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
+import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface SubtitleModalsProps {
showSubtitleModal: boolean;
@@ -65,13 +66,21 @@ const MorphingTab = ({ label, isSelected, onPress }: any) => {
}));
return (
-
+
{label}
-
+
);
};
@@ -112,11 +121,11 @@ export const SubtitleModals: React.FC = ({
if (!showSubtitleModal) return null;
return (
-
+
{/* Backdrop */}
-
+
-
+
{/* Centered Modal Container */}
@@ -149,21 +158,29 @@ export const SubtitleModals: React.FC = ({
{activeTab === 'built-in' && (
- { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }}
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && selectedTextTrack === -1}
>
None
-
+
{ksTextTracks.map((track) => (
- { selectTextTrack(track.id); setSelectedOnlineSubtitleId(null); }}
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === track.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && selectedTextTrack === track.id}
>
{getTrackDisplayName(track)}
{selectedTextTrack === track.id && }
-
+
))}
)}
@@ -171,23 +188,34 @@ export const SubtitleModals: React.FC = ({
{activeTab === 'addon' && (
{availableSubtitles.length === 0 ? (
-
+
Search Online Subtitles
-
+
) : (
availableSubtitles.map((sub) => (
- { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }}
- style={{ padding: 5,paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', textAlignVertical: 'center' }}
+ style={{ padding: 5, paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && selectedOnlineSubtitleId === sub.id}
>
{sub.display}
{formatLanguage(sub.language)}
{selectedOnlineSubtitleId === sub.id && }
-
+
))
)}
@@ -233,7 +261,7 @@ export const SubtitleModals: React.FC = ({
Quick Presets
- {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
@@ -241,33 +269,45 @@ export const SubtitleModals: React.FC = ({
setSubtitleLineHeightMultiplier(1.2);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={20}
>
Default
-
-
+ {
setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={20}
>
Yellow
-
-
+ {
setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={20}
>
High Contrast
-
-
+ {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={20}
>
Large
-
+
@@ -283,15 +323,27 @@ export const SubtitleModals: React.FC = ({
Font Size
-
+
-
+
{subtitleSize}
-
+
-
+
@@ -299,12 +351,15 @@ export const SubtitleModals: React.FC = ({
Show Background
-
-
+
@@ -321,7 +376,14 @@ export const SubtitleModals: React.FC = ({
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
- setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
+ setSubtitleTextColor(c)}
+ style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={11}
+ />
))}
@@ -329,95 +391,175 @@ export const SubtitleModals: React.FC = ({
Align
{([ { key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' } ] as const).map(a => (
- setSubtitleAlign(a.key)} style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}>
+ setSubtitleAlign(a.key)}
+ style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={8}
+ >
-
+
))}
Bottom Offset
- setSubtitleBottomOffset(Math.max(0, subtitleBottomOffset - 5))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleBottomOffset(Math.max(0, subtitleBottomOffset - 5))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
{subtitleBottomOffset}
- setSubtitleBottomOffset(subtitleBottomOffset + 5)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleBottomOffset(subtitleBottomOffset + 5)}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
Background Opacity
- setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
{subtitleBgOpacity.toFixed(1)}
- setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
Text Shadow
- setSubtitleTextShadow(!subtitleTextShadow)} style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleTextShadow ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }}>
+ setSubtitleTextShadow(!subtitleTextShadow)}
+ style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleTextShadow ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={10}
+ >
{subtitleTextShadow ? 'On' : 'Off'}
-
+
Outline Color
{['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => (
- setSubtitleOutlineColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
+ setSubtitleOutlineColor(c)}
+ style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={11}
+ />
))}
Outline Width
- setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
{subtitleOutlineWidth}
- setSubtitleOutlineWidth(subtitleOutlineWidth + 1)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleOutlineWidth(subtitleOutlineWidth + 1)}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
Letter Spacing
- setSubtitleLetterSpacing(Math.max(0, +(subtitleLetterSpacing - 0.5).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleLetterSpacing(Math.max(0, +(subtitleLetterSpacing - 0.5).toFixed(1)))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
{subtitleLetterSpacing.toFixed(1)}
- setSubtitleLetterSpacing(+(subtitleLetterSpacing + 0.5).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleLetterSpacing(+(subtitleLetterSpacing + 0.5).toFixed(1))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
Line Height
- setSubtitleLineHeightMultiplier(Math.max(1, +(subtitleLineHeightMultiplier - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleLineHeightMultiplier(Math.max(1, +(subtitleLineHeightMultiplier - 0.1).toFixed(1)))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
{subtitleLineHeightMultiplier.toFixed(1)}
- setSubtitleLineHeightMultiplier(+(subtitleLineHeightMultiplier + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleLineHeightMultiplier(+(subtitleLineHeightMultiplier + 0.1).toFixed(1))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
@@ -425,21 +567,33 @@ export const SubtitleModals: React.FC = ({
Timing Offset (s)
- setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
{subtitleOffsetSec.toFixed(1)}
- setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
+ setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))}
+ style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={controlBtn.radius}
+ >
-
+
Nudge subtitles earlier (-) or later (+) to sync if needed.
- {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
@@ -447,9 +601,12 @@ export const SubtitleModals: React.FC = ({
setSubtitleLineHeightMultiplier(1.2); setSubtitleOffsetSec(0);
}}
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 8, backgroundColor: 'rgba(255,255,255,0.1)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={8}
>
Reset to defaults
-
+
diff --git a/src/screens/AIChatScreen.tsx b/src/screens/AIChatScreen.tsx
index 34ad7cc..4aeba9b 100644
--- a/src/screens/AIChatScreen.tsx
+++ b/src/screens/AIChatScreen.tsx
@@ -4,7 +4,6 @@ import {
Text,
StyleSheet,
TextInput,
- TouchableOpacity,
ScrollView,
StatusBar,
KeyboardAvoidingView,
@@ -58,6 +57,7 @@ import Animated, {
Extrapolate,
runOnJS
} from 'react-native-reanimated';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
@@ -302,7 +302,7 @@ const SuggestionChip: React.FC = React.memo(({ text, onPres
const { currentTheme } = useTheme();
return (
- = React.memo(({ text, onPres
{text}
-
+
);
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress);
@@ -684,7 +684,7 @@ const AIChatScreen: React.FC = () => {
headerAnimatedStyle
]}>
- {
if (Platform.OS === 'android') {
modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => {
@@ -697,7 +697,7 @@ const AIChatScreen: React.FC = () => {
style={styles.backButton}
>
-
+
@@ -821,7 +821,7 @@ const AIChatScreen: React.FC = () => {
blurOnSubmit={false}
/>
- {
size={20}
color={inputText.trim() ? 'white' : currentTheme.colors.mediumEmphasis}
/>
-
+
diff --git a/src/screens/AccountManageScreen.tsx b/src/screens/AccountManageScreen.tsx
index 6b129dd..a29c70e 100644
--- a/src/screens/AccountManageScreen.tsx
+++ b/src/screens/AccountManageScreen.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native';
+import { View, Text, StyleSheet, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native';
import FastImage from '@d11/react-native-fast-image';
import { useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -9,6 +9,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics';
import CustomAlert from '../components/CustomAlert';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const AccountManageScreen: React.FC = () => {
const navigation = useNavigation();
@@ -97,9 +98,9 @@ const AccountManageScreen: React.FC = () => {
colors={[currentTheme.colors.darkBackground, '#111318']}
style={StyleSheet.absoluteFill}
/>
- navigation.goBack()} style={styles.headerBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
+ navigation.goBack()} style={styles.headerBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
-
+
Account
@@ -185,7 +186,7 @@ const AccountManageScreen: React.FC = () => {
{/* Save and Sign out */}
- {
Save changes
>
)}
-
+
- {
>
Sign out
-
+
{
{reorderMode && (
- moveAddonUp(item)}
disabled={isFirstItem}
@@ -984,8 +984,8 @@ const AddonsScreen = () => {
size={20}
color={isFirstItem ? colors.mediumGray : colors.white}
/>
-
-
+ moveAddonDown(item)}
disabled={isLastItem}
@@ -995,7 +995,7 @@ const AddonsScreen = () => {
size={20}
color={isLastItem ? colors.mediumGray : colors.white}
/>
-
+
)}
@@ -1030,20 +1030,20 @@ const AddonsScreen = () => {
{!reorderMode ? (
<>
{isConfigurable && (
- handleConfigureAddon(item, item.transport)}
>
-
+
)}
{!stremioService.isPreInstalledAddon(item.id) && (
- handleRemoveAddon(item)}
>
-
+
)}
>
) : (
@@ -1098,14 +1098,14 @@ const AddonsScreen = () => {
{isConfigurable && (
- handleConfigureAddon(manifest, transportUrl)}
>
-
+
)}
- handleAddAddon(transportUrl)}
disabled={installing}
@@ -1115,7 +1115,7 @@ const AddonsScreen = () => {
) : (
)}
-
+
);
@@ -1134,17 +1134,17 @@ const AddonsScreen = () => {
{/* Header */}
- navigation.goBack()}
>
Settings
-
+
{/* Reorder Mode Toggle Button */}
-
@@ -1153,10 +1153,10 @@ const AddonsScreen = () => {
size={24}
color={reorderMode ? colors.primary : colors.white}
/>
-
+
{/* Refresh Button */}
- {
size={24}
color={loading ? colors.mediumGray : colors.white}
/>
-
+
@@ -1221,7 +1221,7 @@ const AddonsScreen = () => {
autoCapitalize="none"
autoCorrect={false}
/>
- handleAddAddon()}
disabled={installing || !addonUrl}
@@ -1229,7 +1229,7 @@ const AddonsScreen = () => {
{installing ? 'Loading...' : 'Add Addon'}
-
+
)}
@@ -1289,14 +1289,14 @@ const AddonsScreen = () => {
{promoAddon.behaviorHints?.configurable && (
- handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
>
-
+
)}
- handleAddAddon(PROMO_ADDON_URL)}
disabled={installing}
@@ -1306,7 +1306,7 @@ const AddonsScreen = () => {
) : (
)}
-
+
@@ -1371,14 +1371,14 @@ const AddonsScreen = () => {
{item.manifest.behaviorHints?.configurable && (
- handleConfigureAddon(item.manifest, item.transportUrl)}
>
-
+
)}
- handleAddAddon(item.transportUrl)}
disabled={installing}
@@ -1388,7 +1388,7 @@ const AddonsScreen = () => {
) : (
)}
-
+
@@ -1435,14 +1435,14 @@ const AddonsScreen = () => {
<>
Install Addon
- {
setShowConfirmModal(false);
setAddonDetails(null);
}}
>
-
+
{
- {
setShowConfirmModal(false);
@@ -1512,8 +1512,8 @@ const AddonsScreen = () => {
}}
>
Cancel
-
-
+ {
) : (
Install
)}
-
+
>
)}
diff --git a/src/screens/AuthScreen.tsx b/src/screens/AuthScreen.tsx
index c890208..70b5c3d 100644
--- a/src/screens/AuthScreen.tsx
+++ b/src/screens/AuthScreen.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Dimensions, Animated, Easing, Keyboard } from 'react-native';
+import { View, TextInput, Text, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Dimensions, Animated, Easing, Keyboard } from 'react-native';
import { mmkvStorage } from '../services/mmkvStorage';
import { LinearGradient } from 'expo-linear-gradient';
import { MaterialIcons } from '@expo/vector-icons';
@@ -9,6 +9,7 @@ import { useNavigation, useRoute } from '@react-navigation/native';
import * as Haptics from 'expo-haptics';
import { useToast } from '../contexts/ToastContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window');
@@ -277,9 +278,9 @@ const AuthScreen: React.FC = () => {
]}
>
{navigation.canGoBack() && (
- navigation.goBack()} style={[styles.backButton, Platform.OS === 'android' ? { top: Math.max(insets.top + 6, 18) } : null]} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
+ navigation.goBack()} style={[styles.backButton, Platform.OS === 'android' ? { top: Math.max(insets.top + 6, 18) } : null]} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
-
+
)}
{mode === 'signin' ? 'Welcome back' : 'Create your account'}
@@ -299,7 +300,7 @@ const AuthScreen: React.FC = () => {
},
]}
>
- {
Read more {showWarningDetails ? '▼' : '▶'}
-
+
{/* Expanded Details */}
{showWarningDetails && (
@@ -392,7 +393,7 @@ const AuthScreen: React.FC = () => {
},
]}
/>
- {
Sign In
-
-
+ {
]}>
Sign Up {signupDisabled && '(Disabled)'}
-
+
{/* Email Input */}
@@ -477,13 +478,13 @@ const AuthScreen: React.FC = () => {
returnKeyType="done"
onSubmitEditing={handleSubmit}
/>
- setShowPassword(p => !p)} style={styles.eyeButton}>
+ setShowPassword(p => !p)} style={styles.eyeButton}>
-
+
{Platform.OS !== 'android' && isPasswordValid && (
)}
@@ -515,13 +516,13 @@ const AuthScreen: React.FC = () => {
returnKeyType="done"
onSubmitEditing={handleSubmit}
/>
- setShowConfirm(p => !p)} style={styles.eyeButton}>
+ setShowConfirm(p => !p)} style={styles.eyeButton}>
-
+
{Platform.OS !== 'android' && passwordsMatch && isConfirmValid && (
)}
@@ -539,7 +540,7 @@ const AuthScreen: React.FC = () => {
{/* Submit Button */}
- {
{mode === 'signin' ? 'Sign In' : 'Create Account'}
)}
-
+
{/* Switch Mode */}
{!signupDisabled && (
- setMode(mode === 'signin' ? 'signup' : 'signin')}
activeOpacity={0.7}
style={{ marginTop: 16 }}
@@ -595,7 +596,7 @@ const AuthScreen: React.FC = () => {
{mode === 'signin' ? 'Sign up' : 'Sign in'}
-
+
)}
{/* Signup disabled message */}
@@ -608,7 +609,7 @@ const AuthScreen: React.FC = () => {
)}
{/* Skip sign in - more prominent when coming from onboarding */}
- {
}}>
Continue without an account
-
+
diff --git a/src/screens/BackdropGalleryScreen.tsx b/src/screens/BackdropGalleryScreen.tsx
index b9b71bb..74ee6a3 100644
--- a/src/screens/BackdropGalleryScreen.tsx
+++ b/src/screens/BackdropGalleryScreen.tsx
@@ -4,10 +4,10 @@ import {
Text,
StyleSheet,
FlatList,
- TouchableOpacity,
Dimensions,
ActivityIndicator,
StatusBar,
+ Platform,
} from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import { SafeAreaView } from 'react-native-safe-area-context';
@@ -16,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { TMDBService } from '../services/tmdbService';
import { useTheme } from '../contexts/ThemeContext';
import { useSettings } from '../hooks/useSettings';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width } = Dimensions.get('window');
const BACKDROP_WIDTH = width * 0.9;
@@ -116,12 +117,16 @@ const BackdropGalleryScreen: React.FC = () => {
const renderHeader = () => (
- navigation.goBack()}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={999}
+ hasTVPreferredFocus={Platform.isTV}
>
-
+
{title}
diff --git a/src/screens/BackupScreen.tsx b/src/screens/BackupScreen.tsx
index f6a3ff3..d51ed64 100644
--- a/src/screens/BackupScreen.tsx
+++ b/src/screens/BackupScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
ActivityIndicator,
Platform,
SafeAreaView,
@@ -21,6 +20,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { logger } from '../utils/logger';
import CustomAlert from '../components/CustomAlert';
import { useBackupOptions } from '../hooks/useBackupOptions';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Check if running on TV platform
const isTV = Platform.isTV;
@@ -303,13 +303,13 @@ const BackupScreen: React.FC = () => {
{/* Header */}
- navigation.goBack()}
>
Settings
-
+
{/* Empty for now, but keeping structure consistent */}
@@ -345,7 +345,7 @@ const BackupScreen: React.FC = () => {
{/* Core Data Group */}
- toggleSection('coreData')}
activeOpacity={0.7}
@@ -365,7 +365,7 @@ const BackupScreen: React.FC = () => {
>
-
+
{
{/* Addons & Integrations Group */}
- toggleSection('addonsIntegrations')}
activeOpacity={0.7}
@@ -413,7 +413,7 @@ const BackupScreen: React.FC = () => {
>
-
+
{
{/* Settings & Preferences Group */}
- toggleSection('settingsPreferences')}
activeOpacity={0.7}
@@ -468,7 +468,7 @@ const BackupScreen: React.FC = () => {
>
-
+
{
Backup & Restore
- {
Create Backup
>
)}
-
+
- {
>
Restore from Backup
-
+
{/* Info Section */}
diff --git a/src/screens/CatalogScreen.tsx b/src/screens/CatalogScreen.tsx
index 5932980..7025581 100644
--- a/src/screens/CatalogScreen.tsx
+++ b/src/screens/CatalogScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
ActivityIndicator,
SafeAreaView,
StatusBar,
@@ -42,6 +41,7 @@ import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
import { mmkvStorage } from '../services/mmkvStorage';
import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
import { tmdbService } from '../services/tmdbService';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
type CatalogScreenProps = {
route: RouteProp;
@@ -762,7 +762,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
const aspectRatio = shape === 'landscape' ? 16 / 9 : (shape === 'square' ? 1 : 2 / 3);
return (
- = ({ route, navigation }) => {
]}
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type, addonId })}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="poster"
+ focusBorderRadius={12}
>
= ({ route, navigation }) => {
{item.name}
)}
-
+
);
}, [navigation, styles, effectiveNumColumns, effectiveItemWidth, screenData, type, nowPlayingMovies, colors.white, colors.mediumGray, optimizePosterUrl, addonId, isDarkMode, showTitles]);
@@ -847,12 +850,16 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
No content found
-
Try Again
-
+
);
@@ -862,12 +869,16 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
{error}
- loadItems(true)}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
+ hasTVPreferredFocus={Platform.isTV}
>
Retry
-
+
);
@@ -885,13 +896,17 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
- navigation.goBack()}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV}
>
Back
-
+
{displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
{renderLoadingState()}
@@ -904,13 +919,17 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
- navigation.goBack()}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV}
>
Back
-
+
{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
{renderErrorState()}
@@ -922,13 +941,17 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
- navigation.goBack()}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV}
>
Back
-
+
{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}
@@ -943,18 +966,21 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
{catalogExtras.map(extra => (
{/* All option - clears filter */}
- handleFilterChange(extra.name, undefined)}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={16}
>
All
-
+
{/* Filter options from catalog extra */}
{extra.options?.map(option => {
@@ -962,15 +988,19 @@ const CatalogScreen: React.FC = ({ route, navigation }) => {
? activeGenreFilter === option
: selectedFilters[extra.name] === option;
return (
- handleFilterChange(extra.name, option)}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={16}
+ hasTVPreferredFocus={Platform.isTV && isActive}
>
{option}
-
+
);
})}
diff --git a/src/screens/ContinueWatchingSettingsScreen.tsx b/src/screens/ContinueWatchingSettingsScreen.tsx
index 72bfdca..9ef9f95 100644
--- a/src/screens/ContinueWatchingSettingsScreen.tsx
+++ b/src/screens/ContinueWatchingSettingsScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
ScrollView,
StatusBar,
Platform,
@@ -17,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { useSettings } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// TTL options in milliseconds - organized in rows
const TTL_OPTIONS = [
@@ -132,7 +132,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => {
const isSelected = settings.streamCacheTTL === option.value;
return (
- {
]}
onPress={() => handleUpdateSetting('streamCacheTTL', option.value)}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={8}
+ hasTVPreferredFocus={Platform.isTV && isSelected}
>
{
{isSelected && (
)}
-
+
);
};
@@ -162,13 +166,17 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
{/* Header */}
-
Settings
-
+
diff --git a/src/screens/ContributorsScreen.tsx b/src/screens/ContributorsScreen.tsx
index 0e77958..78688f0 100644
--- a/src/screens/ContributorsScreen.tsx
+++ b/src/screens/ContributorsScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
ScrollView,
SafeAreaView,
StatusBar,
@@ -24,6 +23,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { fetchContributors, GitHubContributor } from '../services/githubReleaseService';
import { RootStackParamList } from '../navigation/AppNavigator';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
@@ -91,7 +91,7 @@ const ContributorCard: React.FC = ({ contributor, currentT
}, [contributor.html_url]);
return (
- = ({ contributor, currentT
color={currentTheme.colors.mediumEmphasis}
style={styles.externalIcon}
/>
-
+
);
};
@@ -164,7 +164,7 @@ const SpecialMentionCard: React.FC = ({ mention, curren
const defaultAvatar = `https://cdn.discordapp.com/embed/avatars/0.png`;
return (
- = ({ mention, curren
color={currentTheme.colors.mediumEmphasis}
style={styles.externalIcon}
/>
-
+
);
};
@@ -422,13 +422,13 @@ const ContributorsScreen: React.FC = () => {
- navigation.goBack()}
>
Settings
-
+
{
- navigation.goBack()}
>
Settings
-
+
{
{ backgroundColor: currentTheme.colors.elevation1 },
isTablet && styles.tabletTabSwitcher
]}>
- {
]}>
Contributors
-
-
+ {
]}>
Special Mentions
-
+
@@ -530,14 +530,14 @@ const ContributorsScreen: React.FC = () => {
GitHub API rate limit exceeded. Please try again later or pull to refresh.
- loadContributors()}
>
Try Again
-
+
) : contributors.length === 0 ? (
diff --git a/src/screens/DebridIntegrationScreen.tsx b/src/screens/DebridIntegrationScreen.tsx
index 193bbf2..7579d47 100644
--- a/src/screens/DebridIntegrationScreen.tsx
+++ b/src/screens/DebridIntegrationScreen.tsx
@@ -4,7 +4,6 @@ import {
Text,
StyleSheet,
TextInput,
- TouchableOpacity,
SafeAreaView,
StatusBar,
Platform,
@@ -27,6 +26,7 @@ import { logger } from '../utils/logger';
import CustomAlert from '../components/CustomAlert';
import { mmkvStorage } from '../services/mmkvStorage';
import axios from 'axios';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const TORBOX_STORAGE_KEY = 'torbox_debrid_config';
@@ -1132,15 +1132,18 @@ const DebridIntegrationScreen = () => {
-
{loading ? 'Disconnecting...' : 'Disconnect & Remove'}
-
+
{userData && (
@@ -1213,12 +1216,15 @@ const DebridIntegrationScreen = () => {
Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.
- Linking.openURL('https://torbox.app/settings?section=integration-settings')}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
>
Open Settings
-
+
>
) : (
@@ -1227,9 +1233,15 @@ const DebridIntegrationScreen = () => {
Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.
- Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')} style={styles.guideLink}>
+ Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')}
+ style={styles.guideLink}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={12}
+ >
What is a Debrid Service?
-
+
Torbox API Key
@@ -1245,24 +1257,33 @@ const DebridIntegrationScreen = () => {
/>
-
{loading ? 'Connecting...' : 'Connect & Install'}
-
+
Unlock Premium Speeds
Get a Torbox subscription to access cached high-quality streams with zero buffering.
-
+
Get Subscription
-
+
>
)}
@@ -1306,12 +1327,15 @@ const DebridIntegrationScreen = () => {
Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.
- Linking.openURL('https://torbox.app/subscription?referral=493192f2-6403-440f-b414-768f72222ec7')}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
>
Get TorBox Subscription
-
+
)}
@@ -1320,13 +1344,17 @@ const DebridIntegrationScreen = () => {
Debrid Service *
{TORRENTIO_DEBRID_SERVICES.map((service: any) => (
- setTorrentioConfig(prev => ({ ...prev, debridService: service.id }))}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && torrentioConfig.debridService === service.id}
>
{
]}>
{service.name}
-
+
))}
@@ -1355,9 +1383,12 @@ const DebridIntegrationScreen = () => {
{/* Sorting - Accordion */}
- toggleSection('sorting')}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={16}
>
Sorting
@@ -1366,29 +1397,36 @@ const DebridIntegrationScreen = () => {
-
+
{expandedSections.sorting && (
{TORRENTIO_SORT_OPTIONS.map(option => (
- setTorrentioConfig(prev => ({ ...prev, sort: option.id }))}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && torrentioConfig.sort === option.id}
>
{option.name}
-
+
))}
)}
{/* Quality Filter - Accordion */}
- toggleSection('qualityFilter')}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={16}
>
Exclude Qualities
@@ -1397,29 +1435,36 @@ const DebridIntegrationScreen = () => {
-
+
{expandedSections.qualityFilter && (
{TORRENTIO_QUALITY_FILTERS.map(quality => (
- toggleQualityFilter(quality.id)}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={999}
+ hasTVPreferredFocus={Platform.isTV && torrentioConfig.qualityFilter.includes(quality.id)}
>
{quality.name}
-
+
))}
)}
{/* Priority Languages - Accordion */}
- toggleSection('languages')}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={16}
>
Priority Languages
@@ -1428,29 +1473,36 @@ const DebridIntegrationScreen = () => {
-
+
{expandedSections.languages && (
{TORRENTIO_LANGUAGES.map(lang => (
- toggleLanguage(lang.id)}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={999}
+ hasTVPreferredFocus={Platform.isTV && torrentioConfig.priorityLanguages.includes(lang.id)}
>
{lang.name}
-
+
))}
)}
{/* Max Results - Accordion */}
- toggleSection('maxResults')}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={16}
>
Max Results
@@ -1459,36 +1511,43 @@ const DebridIntegrationScreen = () => {
-
+
{expandedSections.maxResults && (
{TORRENTIO_MAX_RESULTS.map(option => (
- setTorrentioConfig(prev => ({ ...prev, maxResults: option.id }))}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && torrentioConfig.maxResults === option.id}
>
{option.name}
-
+
))}
)}
{/* Additional Options - Accordion */}
- toggleSection('options')}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={16}
>
Additional Options
Catalog & download settings
-
+
{expandedSections.options && (
@@ -1526,33 +1585,42 @@ const DebridIntegrationScreen = () => {
{torrentioConfig.isInstalled ? (
<>
-
{torrentioLoading ? 'Updating...' : 'Update Configuration'}
-
-
+
Remove Torrentio
-
+
>
) : (
-
{torrentioLoading ? 'Installing...' : 'Install Torrentio'}
-
+
)}
@@ -1578,33 +1646,45 @@ const DebridIntegrationScreen = () => {
- navigation.goBack()}
style={styles.backButton}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={999}
+ hasTVPreferredFocus={Platform.isTV}
>
-
+
Debrid Integration
{/* Tab Selector */}
- setActiveTab('torbox')}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={16}
+ hasTVPreferredFocus={Platform.isTV && activeTab === 'torbox'}
>
TorBox
-
-
+ setActiveTab('torrentio')}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={16}
+ hasTVPreferredFocus={Platform.isTV && activeTab === 'torrentio'}
>
Torrentio
-
+
= 768;
@@ -80,7 +80,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp
Downloaded content will appear here for offline viewing
- {
navigation.navigate('Search');
@@ -89,7 +89,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp
Explore Content
-
+
);
};
@@ -204,7 +204,7 @@ const DownloadItemComponent: React.FC<{
};
return (
- onPress(item)}
onLongPress={handleLongPress}
@@ -314,7 +314,7 @@ const DownloadItemComponent: React.FC<{
{/* Action buttons */}
{getActionIcon() && (
-
-
+
)}
- onRequestRemove(item)}
activeOpacity={0.7}
@@ -337,9 +337,9 @@ const DownloadItemComponent: React.FC<{
size={20}
color={currentTheme.colors.error}
/>
-
+
-
+
);
});
@@ -568,7 +568,7 @@ const DownloadsScreen: React.FC = () => {
);
const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => (
- {
)}
-
+
);
return (
@@ -627,7 +627,7 @@ const DownloadsScreen: React.FC = () => {
{
size={24}
color={currentTheme.colors.mediumEmphasis}
/>
-
+
}
isTablet={isTablet}
>
diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx
index fc6ed8d..0fd3684 100644
--- a/src/screens/HomeScreen.tsx
+++ b/src/screens/HomeScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
ActivityIndicator,
SafeAreaView,
StatusBar,
@@ -77,6 +76,7 @@ import { useToast } from '../contexts/ToastContext';
import FirstTimeWelcome from '../components/FirstTimeWelcome';
import { HeaderVisibility } from '../contexts/HeaderVisibility';
import { useTrailer } from '../contexts/TrailerContext';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Constants
const CATALOG_SETTINGS_KEY = 'catalog_settings';
@@ -799,15 +799,18 @@ const HomeScreen = () => {
return (
-
Load More Catalogs
-
+
);
@@ -829,13 +832,17 @@ const HomeScreen = () => {
No content available
- navigation.navigate('Settings')}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={16}
+ hasTVPreferredFocus={Platform.isTV}
>
Add Catalogs
-
+
)}
>
diff --git a/src/screens/LibraryScreen.tsx b/src/screens/LibraryScreen.tsx
index 83d11fd..b7c920e 100644
--- a/src/screens/LibraryScreen.tsx
+++ b/src/screens/LibraryScreen.tsx
@@ -9,7 +9,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
useColorScheme,
useWindowDimensions,
SafeAreaView,
@@ -38,6 +37,7 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg';
import { traktService, TraktService, TraktImages } from '../services/traktService';
import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
import { useSettings } from '../hooks/useSettings';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
interface LibraryItem extends StreamingContent {
progress?: number;
@@ -121,10 +121,13 @@ const TraktItem = React.memo(({
}, [navigation, item.imdbId, item.type]);
return (
-
@@ -146,7 +149,7 @@ const TraktItem = React.memo(({
)}
-
+
);
});
@@ -386,7 +389,7 @@ const LibraryScreen = () => {
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
const renderItem = ({ item }: { item: LibraryItem }) => (
- navigation.navigate('Metadata', { id: item.id, type: item.type })}
onLongPress={() => {
@@ -394,6 +397,9 @@ const LibraryScreen = () => {
setMenuVisible(true);
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="poster"
+ focusBorderRadius={12}
>
@@ -424,17 +430,20 @@ const LibraryScreen = () => {
)}
-
+
);
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
- {
setSelectedTraktFolder(folder.id);
loadAllCollections();
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="card"
+ focusBorderRadius={12}
>
@@ -452,11 +461,11 @@ const LibraryScreen = () => {
-
+
);
const renderTraktFolder = () => (
- {
if (!traktAuthenticated) {
@@ -468,6 +477,9 @@ const LibraryScreen = () => {
}
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="card"
+ focusBorderRadius={12}
>
@@ -489,7 +501,7 @@ const LibraryScreen = () => {
)}
-
+
);
const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => {
@@ -715,7 +727,7 @@ const LibraryScreen = () => {
Your Trakt collections will appear here once you start using Trakt
- {
loadAllCollections();
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={16}
+ hasTVPreferredFocus={Platform.isTV}
>
Load Collections
-
+
);
}
@@ -756,7 +772,7 @@ const LibraryScreen = () => {
This collection is empty
- {
loadAllCollections();
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={16}
+ hasTVPreferredFocus={Platform.isTV}
>
Refresh
-
+
);
}
@@ -791,7 +811,7 @@ const LibraryScreen = () => {
const isActive = filter === filterType;
return (
- {
setFilter(filterType);
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={18}
+ hasTVPreferredFocus={Platform.isTV && isActive}
>
{filterType === 'trakt' ? (
@@ -833,7 +857,7 @@ const LibraryScreen = () => {
>
{label}
-
+
);
};
@@ -858,16 +882,20 @@ const LibraryScreen = () => {
{emptySubtitle}
- navigation.navigate('Search')}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={16}
+ hasTVPreferredFocus={Platform.isTV}
>
Find something to watch
-
+
);
}
diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx
index bd1b863..5a2400d 100644
--- a/src/screens/MetadataScreen.tsx
+++ b/src/screens/MetadataScreen.tsx
@@ -6,7 +6,6 @@ import {
StatusBar,
ActivityIndicator,
Dimensions,
- TouchableOpacity,
InteractionManager,
BackHandler,
Platform,
@@ -64,6 +63,7 @@ import { useWatchProgress } from '../hooks/useWatchProgress';
import { TraktService, TraktPlaybackItem } from '../services/traktService';
import { tmdbService } from '../services/tmdbService';
import { catalogService } from '../services/catalogService';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { height } = Dimensions.get('window');
@@ -865,21 +865,28 @@ const MetadataScreen: React.FC = () => {
{metadataError}
)}
-
Try Again
-
-
+
Go Back
-
+
);
@@ -1240,7 +1247,7 @@ const MetadataScreen: React.FC = () => {
{/* Backdrop Gallery section - shown after movie details for movies when TMDB ID is available and enrichment is enabled */}
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length === 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
- navigation.navigate('BackdropGallery' as any, {
tmdbId: metadata.tmdbId,
@@ -1248,10 +1255,13 @@ const MetadataScreen: React.FC = () => {
title: metadata.name || 'Gallery'
})}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={16}
>
Backdrop Gallery
-
+
)}
@@ -1381,7 +1391,7 @@ const MetadataScreen: React.FC = () => {
{/* Backdrop Gallery section - shown after show details for TV shows when TMDB ID is available and enrichment is enabled */}
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
- navigation.navigate('BackdropGallery' as any, {
tmdbId: metadata.tmdbId,
@@ -1389,10 +1399,13 @@ const MetadataScreen: React.FC = () => {
title: metadata.name || 'Gallery'
})}
focusable={Platform.isTV}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={16}
>
Backdrop Gallery
-
+
)}
diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx
index fc68202..50f7103 100644
--- a/src/screens/OnboardingScreen.tsx
+++ b/src/screens/OnboardingScreen.tsx
@@ -4,7 +4,6 @@ import {
Text,
StyleSheet,
Dimensions,
- TouchableOpacity,
StatusBar,
Platform,
} from 'react-native';
@@ -25,6 +24,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import { mmkvStorage } from '../services/mmkvStorage';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window');
@@ -281,9 +281,9 @@ const OnboardingScreen = () => {
entering={FadeIn.delay(300).duration(600)}
style={styles.header}
>
-
+
Skip
-
+
{/* Smooth Progress Bar */}
@@ -322,7 +322,7 @@ const OnboardingScreen = () => {
{/* Animated Button */}
- {
{currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Continue'}
-
+
diff --git a/src/screens/PlayerSettingsScreen.tsx b/src/screens/PlayerSettingsScreen.tsx
index 577da4d..9c4a94a 100644
--- a/src/screens/PlayerSettingsScreen.tsx
+++ b/src/screens/PlayerSettingsScreen.tsx
@@ -6,7 +6,6 @@ import {
ScrollView,
SafeAreaView,
Platform,
- TouchableOpacity,
StatusBar,
Switch,
} from 'react-native';
@@ -15,6 +14,7 @@ import { useSettings, AppSettings } from '../hooks/useSettings';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '../contexts/ThemeContext';
import CustomAlert from '../components/CustomAlert';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@@ -38,7 +38,7 @@ const SettingItem: React.FC = ({
const { currentTheme } = useTheme();
return (
- = ({
/>
)}
-
+
);
};
@@ -173,7 +173,7 @@ const PlayerSettingsScreen: React.FC = () => {
/>
- {
Settings
-
+
{/* Empty for now, but ready for future actions */}
diff --git a/src/screens/PluginsScreen.tsx b/src/screens/PluginsScreen.tsx
index f0bf46d..22c36eb 100644
--- a/src/screens/PluginsScreen.tsx
+++ b/src/screens/PluginsScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
Switch,
TextInput,
ScrollView,
@@ -25,6 +24,7 @@ import { useSettings } from '../hooks/useSettings';
import { localScraperService, pluginService, ScraperInfo, RepositoryInfo } from '../services/pluginService';
import { logger } from '../utils/logger';
import { useTheme } from '../contexts/ThemeContext';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width: screenWidth } = Dimensions.get('window');
@@ -773,23 +773,23 @@ const CollapsibleSection: React.FC<{
styles: any;
}> = ({ title, children, isExpanded, onToggle, colors, styles }) => (
-
+
{title}
-
+
{isExpanded && {children}}
);
// Helper component for info tooltips
const InfoTooltip: React.FC<{ text: string; colors: any }> = ({ text, colors }) => (
-
+
-
+
);
// Helper component for status badges
@@ -1361,22 +1361,22 @@ const PluginsScreen: React.FC = () => {
{/* Header */}
- navigation.goBack()}
>
Settings
-
+
{/* Help Button */}
- setShowHelpModal(true)}
>
-
+
@@ -1485,7 +1485,7 @@ const PluginsScreen: React.FC = () => {
{repo.id !== currentRepositoryId && (
- handleSwitchRepository(repo.id)}
disabled={switchingRepository === repo.id}
@@ -1495,9 +1495,9 @@ const PluginsScreen: React.FC = () => {
) : (
Switch
)}
-
+
)}
- handleRefreshRepository()}
disabled={isRefreshing || switchingRepository !== null}
@@ -1507,14 +1507,14 @@ const PluginsScreen: React.FC = () => {
) : (
Refresh
)}
-
-
+ handleRemoveRepository(repo.id)}
disabled={switchingRepository !== null}
>
Remove
-
+
))}
@@ -1523,13 +1523,13 @@ const PluginsScreen: React.FC = () => {
{/* Add Repository Button */}
- setShowAddRepositoryModal(true)}
disabled={!settings.enableLocalScrapers || switchingRepository !== null}
>
Add New Repository
-
+
{/* Available Plugins */}
@@ -1553,16 +1553,16 @@ const PluginsScreen: React.FC = () => {
placeholderTextColor={colors.mediumGray}
/>
{searchQuery.length > 0 && (
- setSearchQuery('')}>
+ setSearchQuery('')}>
-
+
)}
{/* Filter Chips */}
{['all', 'movie', 'tv'].map((filter) => (
- {
]}>
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
-
+
))}
{/* Bulk Actions */}
{filteredScrapers.length > 0 && (
- handleBulkToggle(true)}
disabled={isRefreshing}
>
Enable All
-
-
+ handleBulkToggle(false)}
disabled={isRefreshing}
>
Disable All
-
+
)}
>
@@ -1620,12 +1620,12 @@ const PluginsScreen: React.FC = () => {
}
{searchQuery && (
- setSearchQuery('')}
>
Clear Search
-
+
)}
) : (
@@ -1713,14 +1713,14 @@ const PluginsScreen: React.FC = () => {
numberOfLines={1}
/>
{showboxSavedToken.length > 0 && (
- setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
+ setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
-
+
)}
{showboxUiToken !== showboxSavedToken && (
- {
if (showboxScraperId) {
@@ -1731,9 +1731,9 @@ const PluginsScreen: React.FC = () => {
}}
>
Save
-
+
)}
- {
setShowboxUiToken('');
@@ -1744,7 +1744,7 @@ const PluginsScreen: React.FC = () => {
}}
>
Clear
-
+
)}
@@ -1849,7 +1849,7 @@ const PluginsScreen: React.FC = () => {
{qualityOptions.map((quality) => {
const isExcluded = (settings.excludedQualities || []).includes(quality);
return (
- {
]}>
{isExcluded ? '✕ ' : ''}{quality}
-
+
);
})}
@@ -1898,7 +1898,7 @@ const PluginsScreen: React.FC = () => {
{languageOptions.map((language) => {
const isExcluded = (settings.excludedLanguages || []).includes(language);
return (
- {
]}>
{isExcluded ? '✕ ' : ''}{language}
-
+
);
})}
@@ -1964,12 +1964,12 @@ const PluginsScreen: React.FC = () => {
4. Enable Scrapers - Turn on the scrapers you want to use for streaming
- setShowHelpModal(false)}
>
Got it!
-
+
@@ -2011,7 +2011,7 @@ const PluginsScreen: React.FC = () => {
{/* Action Buttons */}
- {
setShowAddRepositoryModal(false);
@@ -2019,9 +2019,9 @@ const PluginsScreen: React.FC = () => {
}}
>
Cancel
-
+
- {
) : (
Add
)}
-
+
diff --git a/src/screens/ProfilesScreen.tsx b/src/screens/ProfilesScreen.tsx
index 56a7dbb..62b0f36 100644
--- a/src/screens/ProfilesScreen.tsx
+++ b/src/screens/ProfilesScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
FlatList,
StatusBar,
Platform,
@@ -17,6 +16,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useTraktContext } from '../contexts/TraktContext';
import { mmkvStorage } from '../services/mmkvStorage';
import CustomAlert from '../components/CustomAlert';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const PROFILE_STORAGE_KEY = 'user_profiles';
@@ -183,7 +183,7 @@ const ProfilesScreen: React.FC = () => {
const renderItem = ({ item }: { item: Profile }) => (
- {
)}
{!item.isActive && (
- handleDeleteProfile(item.id)}
>
-
+
)}
-
+
);
@@ -227,7 +227,7 @@ const ProfilesScreen: React.FC = () => {
- {
size={24}
color={currentTheme.colors.text}
/>
-
+
{
}
ListFooterComponent={
- {
Add New Profile
-
+
}
/>
@@ -307,7 +307,7 @@ const ProfilesScreen: React.FC = () => {
/>
- {
setNewProfileName('');
@@ -315,8 +315,8 @@ const ProfilesScreen: React.FC = () => {
}}
>
Cancel
-
-
+ {
onPress={handleAddProfile}
>
Create
-
+
diff --git a/src/screens/SearchScreen.tsx b/src/screens/SearchScreen.tsx
index 82a2711..fb836fa 100644
--- a/src/screens/SearchScreen.tsx
+++ b/src/screens/SearchScreen.tsx
@@ -5,7 +5,6 @@ import {
StyleSheet,
TextInput,
FlatList,
- TouchableOpacity,
ActivityIndicator,
useColorScheme,
SafeAreaView,
@@ -44,6 +43,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext';
import LoadingSpinner from '../components/common/LoadingSpinner';
import ScreenHeader from '../components/common/ScreenHeader';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window');
@@ -78,7 +78,7 @@ const MAX_RECENT_SEARCHES = 10;
const PLACEHOLDER_POSTER = 'https://placehold.co/300x450/222222/CCCCCC?text=No+Poster';
-const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
+// NOTE: AnimatedTouchable was unused; keep focus wrapper for TV instead.
const SkeletonLoader = () => {
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
@@ -570,24 +570,29 @@ const SearchScreen = () => {
Recent Searches
{recentSearches.map((search, index) => (
- {
- setQuery(search);
- Keyboard.dismiss();
- }}
- >
+
-
- {search}
-
- {
+ setQuery(search);
+ Keyboard.dismiss();
+ }}
+ activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={12}
+ >
+
+ {search}
+
+
+ {
const newRecentSearches = [...recentSearches];
newRecentSearches.splice(index, 1);
@@ -596,10 +601,15 @@ const SearchScreen = () => {
}}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={styles.recentSearchDeleteButton}
+ activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="icon"
+ focusBorderRadius={12}
+ hasTVPreferredFocus={Platform.isTV && index === 0}
>
-
-
+
+
))}
);
@@ -651,7 +661,7 @@ const SearchScreen = () => {
}, [item.id, item.type]);
return (
- {
navigation.navigate('Metadata', { id: item.id, type: item.type });
@@ -663,6 +673,9 @@ const SearchScreen = () => {
}}
delayLongPress={300}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="poster"
+ focusBorderRadius={12}
>
{
{item.year}
)}
-
+
);
};
@@ -934,17 +947,21 @@ const SearchScreen = () => {
ref={inputRef}
/>
{query.length > 0 && (
-
-
+
)}
diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx
index adbc5b2..0bf37bb 100644
--- a/src/screens/SettingsScreen.tsx
+++ b/src/screens/SettingsScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
Switch,
ScrollView,
SafeAreaView,
@@ -56,6 +55,7 @@ import TraktIcon from '../components/icons/TraktIcon';
import TMDBIcon from '../components/icons/TMDBIcon';
import MDBListIcon from '../components/icons/MDBListIcon';
import { campaignService } from '../services/campaignService';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window');
const isTablet = width >= 768;
@@ -144,7 +144,7 @@ const SettingItem: React.FC = ({
const { currentTheme } = useTheme();
return (
- = ({
{ borderBottomColor: currentTheme.colors.elevation2 },
isTablet && styles.tabletSettingItem
]}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={isTablet ? 18 : 16}
>
= ({
{renderControl()}
)}
-
+
);
};
@@ -237,7 +240,7 @@ const Sidebar: React.FC = ({ selectedCategory, onCategorySelect, c
{categories.map((category) => (
- = ({ selectedCategory, onCategorySelect, c
]}
onPress={() => onCategorySelect(category.id)}
activeOpacity={0.6}
+ enableTVFocus={Platform.isTV}
+ preset="listRow"
+ focusBorderRadius={14}
+ hasTVPreferredFocus={Platform.isTV && selectedCategory === category.id}
>
= ({ selectedCategory, onCategorySelect, c
]}>
{category.title}
-
+
))}
@@ -959,7 +966,7 @@ const SettingsScreen: React.FC = () => {
)}
- {
const url = 'https://ko-fi.com/tapframe';
@@ -973,19 +980,25 @@ const SettingsScreen: React.FC = () => {
}
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
>
-
+
- Linking.openURL('https://discord.gg/6w8dr3TSDN')}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
>
{
Discord
-
+
- Linking.openURL('https://www.reddit.com/r/Nuvio/')}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
>
{
Reddit
-
+
@@ -1093,7 +1109,7 @@ const SettingsScreen: React.FC = () => {
{/* Support & Community Buttons */}
- {
const url = 'https://ko-fi.com/tapframe';
@@ -1107,19 +1123,25 @@ const SettingsScreen: React.FC = () => {
}
}}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
>
-
+
- Linking.openURL('https://discord.gg/6w8dr3TSDN')}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
>
{
Discord
-
+
- Linking.openURL('https://www.reddit.com/r/Nuvio/')}
activeOpacity={0.7}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={14}
>
{
Reddit
-
+
diff --git a/src/screens/ShowRatingsScreen.tsx b/src/screens/ShowRatingsScreen.tsx
index 07f82a7..b16aff4 100644
--- a/src/screens/ShowRatingsScreen.tsx
+++ b/src/screens/ShowRatingsScreen.tsx
@@ -6,7 +6,6 @@ import {
ScrollView,
ActivityIndicator,
SafeAreaView,
- TouchableOpacity,
Platform,
StatusBar,
} from 'react-native';
@@ -34,6 +33,7 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import axios from 'axios';
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { logger } from '../utils/logger';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
type RootStackParamList = {
ShowRatings: { showId: number };
@@ -140,7 +140,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
{['tmdb', 'imdb', 'tvmaze'].map((source) => {
const isActive = ratingSource === source;
return (
- setRatingSource(source as RatingSource)}
+ enableTVFocus={Platform.isTV}
+ preset="pill"
+ focusBorderRadius={8}
+ hasTVPreferredFocus={Platform.isTV && isActive}
>
{source.toUpperCase()}
-
+
);
})}
diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx
index 77e77d3..c5f4b89 100644
--- a/src/screens/StreamsScreen.tsx
+++ b/src/screens/StreamsScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
ActivityIndicator,
FlatList,
SectionList,
@@ -58,6 +57,7 @@ import StreamCard from '../components/StreamCard';
import AnimatedImage from '../components/AnimatedImage';
import AnimatedText from '../components/AnimatedText';
import AnimatedView from '../components/AnimatedView';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Lazy-safe community blur import for Android
let AndroidBlurView: any = null;
@@ -1850,19 +1850,23 @@ export const StreamsScreen = () => {
-
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? 'Back to Episodes' : 'Back to Info'}
-
+
)}
@@ -2105,12 +2109,16 @@ export const StreamsScreen = () => {
Please add streaming sources in settings
- navigation.navigate('Addons')}
+ enableTVFocus={Platform.isTV}
+ preset="button"
+ focusBorderRadius={16}
+ hasTVPreferredFocus={Platform.isTV}
>
Add Sources
-
+
) : streamsEmpty ? (
showInitialLoading ? (
diff --git a/src/screens/TMDBSettingsScreen.tsx b/src/screens/TMDBSettingsScreen.tsx
index bef2ad1..2d8430a 100644
--- a/src/screens/TMDBSettingsScreen.tsx
+++ b/src/screens/TMDBSettingsScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
TextInput,
SafeAreaView,
StatusBar,
@@ -29,6 +28,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import CustomAlert from '../components/CustomAlert';
// (duplicate import removed)
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
@@ -516,13 +516,13 @@ const TMDBSettingsScreen = () => {
- navigation.goBack()}
>
Settings
-
+
TMDb Settings
@@ -592,12 +592,12 @@ const TMDBSettingsScreen = () => {
Current: {(settings.tmdbLanguagePreference || 'en').toUpperCase()}
- setLanguagePickerVisible(true)}
style={[styles.languageButton, { backgroundColor: currentTheme.colors.primary }]}
>
Change
-
+
{/* Logo Preview */}
@@ -617,7 +617,7 @@ const TMDBSettingsScreen = () => {
style={styles.showsScrollView}
>
{EXAMPLE_SHOWS.map((show) => (
- {
>
{show.name}
-
+
))}
@@ -725,29 +725,29 @@ const TMDBSettingsScreen = () => {
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
/>
-
-
+
-
Save
-
+
{isKeySet && (
-
Clear
-
+
)}
@@ -771,7 +771,7 @@ const TMDBSettingsScreen = () => {
)}
-
@@ -779,7 +779,7 @@ const TMDBSettingsScreen = () => {
How to get a TMDb API key?
-
+
>
)}
@@ -805,7 +805,7 @@ const TMDBSettingsScreen = () => {
-
@@ -813,7 +813,7 @@ const TMDBSettingsScreen = () => {
Clear Cache
-
+
@@ -856,9 +856,9 @@ const TMDBSettingsScreen = () => {
autoCorrect={false}
/>
{languageSearch.length > 0 && (
- setLanguageSearch('')} style={styles.searchClearButton}>
+ setLanguageSearch('')} style={styles.searchClearButton}>
-
+
)}
@@ -880,7 +880,7 @@ const TMDBSettingsScreen = () => {
{ code: 'de', label: 'DE' },
{ code: 'tr', label: 'TR' },
].map(({ code, label }) => (
- { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
style={[
@@ -899,7 +899,7 @@ const TMDBSettingsScreen = () => {
]}>
{label}
-
+
))}
@@ -956,7 +956,7 @@ const TMDBSettingsScreen = () => {
return (
<>
{filteredLanguages.map(({ code, label, native }) => (
- { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
style={[
@@ -992,7 +992,7 @@ const TMDBSettingsScreen = () => {
)}
-
+
))}
{languageSearch.length > 0 && filteredLanguages.length === 0 && (
@@ -1000,12 +1000,12 @@ const TMDBSettingsScreen = () => {
No languages found for "{languageSearch}"
- setLanguageSearch('')}
style={[styles.clearSearchButton, { backgroundColor: currentTheme.colors.elevation1 }]}
>
Clear search
-
+
)}
>
@@ -1016,18 +1016,18 @@ const TMDBSettingsScreen = () => {
{/* Footer Actions */}
- setLanguagePickerVisible(false)}
style={styles.cancelButton}
>
Cancel
-
-
+ setLanguagePickerVisible(false)}
style={[styles.doneButton, { backgroundColor: currentTheme.colors.primary }]}
>
Done
-
+
diff --git a/src/screens/ThemeScreen.tsx b/src/screens/ThemeScreen.tsx
index d9608d9..d419888 100644
--- a/src/screens/ThemeScreen.tsx
+++ b/src/screens/ThemeScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
Switch,
ScrollView,
Platform,
@@ -24,6 +23,7 @@ import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext';
import { RootStackParamList } from '../navigation/AppNavigator';
import { useSettings } from '../hooks/useSettings';
import CustomAlert from '../components/CustomAlert';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width } = Dimensions.get('window');
@@ -53,7 +53,7 @@ const ThemeCard: React.FC = ({
onDelete
}) => {
return (
- = ({
{theme.isEditable && (
{onEdit && (
-
-
+
)}
{onDelete && (
-
-
+
)}
)}
-
+
);
};
@@ -120,7 +120,7 @@ const FilterTab: React.FC = ({
onPress,
primaryColor
}) => (
- = ({
>
{category.name}
-
+
);
type ColorKey = 'primary' | 'secondary' | 'darkBackground';
@@ -242,12 +242,12 @@ const ThemeColorEditor: React.FC
-
-
+
-
Save
-
+
@@ -268,7 +268,7 @@ const ThemeColorEditor: React.FC
- setSelectedColorKey('primary')}
>
Primary
-
+
- setSelectedColorKey('secondary')}
>
Secondary
-
+
- setSelectedColorKey('darkBackground')}
>
Background
-
+
@@ -535,7 +535,7 @@ const ThemeScreen: React.FC = () => {
- navigation.goBack()}
>
@@ -543,7 +543,7 @@ const ThemeScreen: React.FC = () => {
Settings
-
+
{/* Empty for now, but ready for future actions */}
@@ -595,7 +595,7 @@ const ThemeScreen: React.FC = () => {
))}
- {
>
Create Custom Theme
-
+
OPTIONS
diff --git a/src/screens/TraktSettingsScreen.tsx b/src/screens/TraktSettingsScreen.tsx
index a70a50f..195b11d 100644
--- a/src/screens/TraktSettingsScreen.tsx
+++ b/src/screens/TraktSettingsScreen.tsx
@@ -3,7 +3,6 @@ import {
View,
Text,
StyleSheet,
- TouchableOpacity,
ActivityIndicator,
SafeAreaView,
ScrollView,
@@ -24,6 +23,7 @@ import { useTraktIntegration } from '../hooks/useTraktIntegration';
import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings';
import { colors } from '../styles';
import CustomAlert from '../components/CustomAlert';
+import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Check if running on TV platform
const isTV = Platform.isTV;
@@ -246,7 +246,7 @@ const TraktSettingsScreen: React.FC = () => {
]}>
- navigation.goBack()}
style={styles.backButton}
>
@@ -258,7 +258,7 @@ const TraktSettingsScreen: React.FC = () => {
Settings
-
+
{/* Empty for now, but ready for future actions */}
@@ -328,7 +328,7 @@ const TraktSettingsScreen: React.FC = () => {
- {
onPress={handleSignOut}
>
Sign Out
-
+
) : (
@@ -358,7 +358,7 @@ const TraktSettingsScreen: React.FC = () => {
]}>
Sync your watch history, watchlist, and collection with Trakt.tv
- {
Sign In with Trakt
)}
-
+
)}
@@ -448,7 +448,7 @@ const TraktSettingsScreen: React.FC = () => {
- {
Sync Now
)}
-
+
{/* Display Settings Section */}
= {
+ card: { focusScale: 1.05, focusRingWidth: 3, focusBorderRadius: 16 },
+ poster: { focusScale: 1.06, focusRingWidth: 3, focusBorderRadius: 12 },
+ pill: { focusScale: 1.04, focusRingWidth: 3, focusBorderRadius: 999 },
+ button: { focusScale: 1.04, focusRingWidth: 3, focusBorderRadius: 18 },
+ icon: { focusScale: 1.06, focusRingWidth: 3, focusBorderRadius: 999 },
+ listRow: { focusScale: 1.03, focusRingWidth: 3, focusBorderRadius: 14 },
+};
+