tv focus highlight everywhere

Replace remaining screen touchables with focusable wrappers and add shared TV focus presets for consistent, visible focus rings across the app.
This commit is contained in:
tapframe 2025-12-26 19:07:29 +05:30
parent 18c18257c9
commit 86d3035de6
50 changed files with 1549 additions and 583 deletions

View file

@ -5,7 +5,6 @@ import {
Text, Text,
StyleSheet, StyleSheet,
Pressable, Pressable,
TouchableOpacity,
useColorScheme, useColorScheme,
Platform, Platform,
} from 'react-native'; } from 'react-native';
@ -16,6 +15,7 @@ import Animated, {
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { Portal } from 'react-native-paper'; import { Portal } from 'react-native-paper';
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
interface CustomAlertProps { interface CustomAlertProps {
visible: boolean; visible: boolean;
@ -120,7 +120,7 @@ export const CustomAlert = ({
{actions.map((action, idx) => { {actions.map((action, idx) => {
const isPrimary = idx === actions.length - 1; const isPrimary = idx === actions.length - 1;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={action.label} key={action.label}
style={[ style={[
styles.actionButton, styles.actionButton,
@ -132,6 +132,10 @@ export const CustomAlert = ({
]} ]}
onPress={() => handleActionPress(action)} onPress={() => handleActionPress(action)}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && visible && isPrimary}
> >
<Text style={[ <Text style={[
styles.actionText, styles.actionText,
@ -141,7 +145,7 @@ export const CustomAlert = ({
]}> ]}>
{action.label} {action.label}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
})} })}
</View> </View>

View file

@ -1,5 +1,6 @@
import React, { memo, useCallback } from 'react'; import React, { memo, useCallback, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, FlatList } from 'react-native'; import { View, Text, StyleSheet, FlatList, Platform } from 'react-native';
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
interface ProviderFilterProps { interface ProviderFilterProps {
selectedProvider: string; selectedProvider: string;
@ -15,14 +16,24 @@ const ProviderFilter = memo(({
theme theme
}: ProviderFilterProps) => { }: ProviderFilterProps) => {
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
const listRef = useRef<FlatList<any> | null>(null);
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => ( const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.filterChip, styles.filterChip,
selectedProvider === item.id && styles.filterChipSelected selectedProvider === item.id && styles.filterChipSelected
]} ]}
onPress={() => onSelect(item.id)} onPress={() => 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 { }
}}
> >
<Text style={[ <Text style={[
styles.filterChipText, styles.filterChipText,
@ -30,12 +41,13 @@ const ProviderFilter = memo(({
]}> ]}>
{item.name} {item.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
), [selectedProvider, onSelect, styles]); ), [selectedProvider, onSelect, styles]);
return ( return (
<View> <View>
<FlatList <FlatList
ref={listRef}
data={providers} data={providers}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={item => item.id} keyExtractor={item => item.id}

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
ActivityIndicator, ActivityIndicator,
Platform, Platform,
Clipboard, Clipboard,
@ -16,6 +15,7 @@ import QualityBadge from './metadata/QualityBadge';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { useDownloads } from '../contexts/DownloadsContext'; import { useDownloads } from '../contexts/DownloadsContext';
import { useToast } from '../contexts/ToastContext'; import { useToast } from '../contexts/ToastContext';
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
interface StreamCardProps { interface StreamCardProps {
stream: Stream; stream: Stream;
@ -177,16 +177,22 @@ const StreamCard = memo(({
const isDebrid = streamInfo.isDebrid; const isDebrid = streamInfo.isDebrid;
return ( return (
<TouchableOpacity <View
style={[ style={[
styles.streamCard, styles.streamCard,
isLoading && styles.streamCardLoading, isLoading && styles.streamCardLoading,
isDebrid && styles.streamCardHighlighted isDebrid && styles.streamCardHighlighted,
]} ]}
>
<FocusableTouchableOpacity
style={{ flex: 1 }}
onPress={onPress} onPress={onPress}
onLongPress={handleLongPress} onLongPress={handleLongPress}
disabled={isLoading} disabled={isLoading}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
> >
{/* Scraper Logo */} {/* Scraper Logo */}
{showLogos && scraperLogo && ( {showLogos && scraperLogo && (
@ -250,21 +256,23 @@ const StreamCard = memo(({
</View> </View>
</View> </View>
</FocusableTouchableOpacity>
{settings?.enableDownloads !== false && (
<TouchableOpacity {settings?.enableDownloads !== false && (
style={[styles.streamAction, { marginLeft: 8, backgroundColor: theme.colors.elevation2 }]} <View style={{ justifyContent: 'center', marginLeft: 8 }}>
<FocusableTouchableOpacity
style={[styles.streamAction, { backgroundColor: theme.colors.elevation2 }]}
onPress={handleDownload} onPress={handleDownload}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={15}
> >
<MaterialIcons <MaterialIcons name="download" size={20} color={theme.colors.highEmphasis} />
name="download" </FocusableTouchableOpacity>
size={20} </View>
color={theme.colors.highEmphasis} )}
/> </View>
</TouchableOpacity>
)}
</TouchableOpacity>
); );
}); });

View file

@ -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<PressableProps, 'style'> & {
containerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle> | ((state: { pressed: boolean }) => StyleProp<ViewStyle>);
preset?: TVFocusPresetName;
enableTVFocus?: boolean;
focusBorderRadius?: number;
focusScale?: number;
focusRingWidth?: number;
focusRingColor?: string;
};
export const FocusablePressable: React.FC<FocusablePressableProps> = ({
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 (
<Animated.View
style={[
containerStyle,
containerAnimatedStyle,
isFocused && enableTVFocus && {
shadowColor: ringColor,
shadowOpacity: 0.55,
shadowRadius: 14,
shadowOffset: { width: 0, height: 0 },
elevation: 14,
},
]}
>
<Pressable
{...rest}
focusable={rest.focusable ?? enableTVFocus}
onFocus={handleFocus}
onBlur={handleBlur}
style={(state) => {
const base = typeof style === 'function' ? style({ pressed: state.pressed }) : style;
return [base, { position: 'relative' } as ViewStyle] as any;
}}
>
{children}
{enableTVFocus && (
<>
<Animated.View
pointerEvents="none"
style={[
StyleSheet.absoluteFillObject,
styles.focusRing,
{
borderColor: ringColor,
borderWidth: resolvedRingWidth,
borderRadius: resolvedBorderRadius,
},
ringAnimatedStyle,
]}
/>
<View
pointerEvents="none"
style={[
StyleSheet.absoluteFillObject,
{
borderRadius: resolvedBorderRadius,
backgroundColor: isFocused ? 'rgba(255,255,255,0.06)' : 'transparent',
},
]}
/>
</>
)}
</Pressable>
</Animated.View>
);
};
const styles = StyleSheet.create({
focusRing: {
top: 0,
left: 0,
right: 0,
bottom: 0,
},
});

View file

@ -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<TouchableOpacityProps, 'style'> & {
/**
* Optional style applied to the outer Animated wrapper.
* Useful when the touchable itself is absolutely positioned (e.g. overlays).
*/
containerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle>;
/** 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<FocusableTouchableOpacityProps> = ({
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 (
<TouchableOpacity {...rest} activeOpacity={finalActiveOpacity} style={style} onFocus={handleFocus} onBlur={handleBlur}>
{children}
</TouchableOpacity>
);
}
return (
<Animated.View
style={[
containerStyle,
containerAnimatedStyle,
isFocused && {
shadowColor: ringColor,
shadowOpacity: 0.55,
shadowRadius: 14,
shadowOffset: { width: 0, height: 0 },
elevation: 14,
},
]}
>
<TouchableOpacity
{...rest}
focusable={rest.focusable ?? enableTVFocus}
activeOpacity={finalActiveOpacity}
onFocus={handleFocus}
onBlur={handleBlur}
style={[style, { position: 'relative' } as ViewStyle]}
>
{children}
<Animated.View
pointerEvents="none"
style={[
StyleSheet.absoluteFillObject,
styles.focusRing,
{
borderColor: ringColor,
borderWidth: resolvedRingWidth,
borderRadius: resolvedBorderRadius,
},
ringAnimatedStyle,
]}
/>
{/* Slight inner highlight to make focus readable on very bright posters */}
<View
pointerEvents="none"
style={[
StyleSheet.absoluteFillObject,
{
borderRadius: resolvedBorderRadius,
backgroundColor: isFocused ? 'rgba(255,255,255,0.06)' : 'transparent',
},
]}
/>
</TouchableOpacity>
</Animated.View>
);
};
const styles = StyleSheet.create({
focusRing: {
// Keep ring inside bounds to avoid overflow clipping.
top: 0,
left: 0,
right: 0,
bottom: 0,
},
});

View file

@ -3,13 +3,13 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
StatusBar, StatusBar,
Platform, Platform,
} from 'react-native'; } from 'react-native';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Feather, MaterialIcons } from '@expo/vector-icons'; import { Feather, MaterialIcons } from '@expo/vector-icons';
import { FocusableTouchableOpacity } from './FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@ -131,17 +131,20 @@ const ScreenHeader: React.FC<ScreenHeaderProps> = ({
> >
<View style={styles.headerContent}> <View style={styles.headerContent}>
{showBackButton ? ( {showBackButton ? (
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={onBackPress} onPress={onBackPress}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={999}
> >
<IconComponent <IconComponent
name={backIconName as any} name={backIconName as any}
size={24} size={24}
color={currentTheme.colors.text} color={currentTheme.colors.text}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
) : null} ) : null}
{titleComponent ? ( {titleComponent ? (
@ -164,17 +167,20 @@ const ScreenHeader: React.FC<ScreenHeaderProps> = ({
{rightActionComponent ? ( {rightActionComponent ? (
<View style={styles.rightActionContainer}>{rightActionComponent}</View> <View style={styles.rightActionContainer}>{rightActionComponent}</View>
) : rightActionIcon && onRightActionPress ? ( ) : rightActionIcon && onRightActionPress ? (
<TouchableOpacity <FocusableTouchableOpacity
style={styles.rightActionButton} style={styles.rightActionButton}
onPress={onRightActionPress} onPress={onRightActionPress}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={999}
> >
<IconComponent <IconComponent
name={rightActionIcon as any} name={rightActionIcon as any}
size={24} size={24}
color={currentTheme.colors.text} color={currentTheme.colors.text}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
) : ( ) : (
<View style={styles.rightActionPlaceholder} /> <View style={styles.rightActionPlaceholder} />
)} )}

View file

@ -45,6 +45,7 @@ import { useTraktContext } from '../../contexts/TraktContext';
import { BlurView as ExpoBlurView } from 'expo-blur'; import { BlurView as ExpoBlurView } from 'expo-blur';
import { useWatchProgress } from '../../hooks/useWatchProgress'; import { useWatchProgress } from '../../hooks/useWatchProgress';
import { streamCacheService } from '../../services/streamCacheService'; import { streamCacheService } from '../../services/streamCacheService';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
interface AppleTVHeroProps { interface AppleTVHeroProps {
featuredContent: StreamingContent | null; featuredContent: StreamingContent | null;
@ -1177,7 +1178,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
style={logoAnimatedStyle} style={logoAnimatedStyle}
> >
{currentItem.logo && !logoError[currentIndex] ? ( {currentItem.logo && !logoError[currentIndex] ? (
<TouchableOpacity <FocusableTouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
onPress={() => { onPress={() => {
if (currentItem) { if (currentItem) {
@ -1188,6 +1189,10 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
} }
}} }}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={16}
focusRingWidth={3}
focusScale={1.03}
> >
<View <View
style={[ style={[
@ -1211,9 +1216,9 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
}} }}
/> />
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
) : ( ) : (
<TouchableOpacity <FocusableTouchableOpacity
activeOpacity={0.8} activeOpacity={0.8}
onPress={() => { onPress={() => {
if (currentItem) { if (currentItem) {
@ -1224,13 +1229,17 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
} }
}} }}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={16}
focusRingWidth={3}
focusScale={1.03}
> >
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={styles.title} numberOfLines={2}> <Text style={styles.title} numberOfLines={2}>
{currentItem.name} {currentItem.name}
</Text> </Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</Animated.View> </Animated.View>
@ -1253,12 +1262,16 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
{/* Action Buttons - Play and Save buttons */} {/* Action Buttons - Play and Save buttons */}
<View style={styles.buttonsContainer}> <View style={styles.buttonsContainer}>
{/* Play Button */} {/* Play Button */}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.playButton]} style={[styles.playButton]}
onPress={handlePlayAction} onPress={handlePlayAction}
activeOpacity={0.85} activeOpacity={0.85}
hasTVPreferredFocus={Platform.isTV} hasTVPreferredFocus={Platform.isTV}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={18}
focusRingWidth={3}
focusScale={1.04}
> >
<MaterialIcons <MaterialIcons
name={playButtonText === 'Resume' ? "replay" : "play-arrow"} name={playButtonText === 'Resume' ? "replay" : "play-arrow"}
@ -1266,21 +1279,25 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
color="#000" color="#000"
/> />
<Text style={styles.playButtonText}>{playButtonText}</Text> <Text style={styles.playButtonText}>{playButtonText}</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
{/* Save Button */} {/* Save Button */}
<TouchableOpacity <FocusableTouchableOpacity
style={styles.saveButton} style={styles.saveButton}
onPress={handleSaveAction} onPress={handleSaveAction}
activeOpacity={0.85} activeOpacity={0.85}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={18}
focusRingWidth={3}
focusScale={1.04}
> >
<MaterialIcons <MaterialIcons
name={inLibrary ? "bookmark" : "bookmark-outline"} name={inLibrary ? "bookmark" : "bookmark-outline"}
size={24} size={24}
color="white" color="white"
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
{/* Pagination Dots */} {/* Pagination Dots */}

View file

@ -13,6 +13,7 @@ import { storageService } from '../../services/storageService';
import { TraktService } from '../../services/traktService'; import { TraktService } from '../../services/traktService';
import { useTraktContext } from '../../contexts/TraktContext'; import { useTraktContext } from '../../contexts/TraktContext';
import Animated, { FadeIn } from 'react-native-reanimated'; import Animated, { FadeIn } from 'react-native-reanimated';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
interface ContentItemProps { interface ContentItemProps {
item: StreamingContent; item: StreamingContent;
@ -302,12 +303,18 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
return ( return (
<> <>
<Animated.View style={[styles.itemContainer, { width: finalWidth }]} entering={FadeIn.duration(300)}> <Animated.View style={[styles.itemContainer, { width: finalWidth }]} entering={FadeIn.duration(300)}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.contentItem, { width: finalWidth, aspectRatio: finalAspectRatio, borderRadius }]} style={[styles.contentItem, { width: finalWidth, aspectRatio: finalAspectRatio, borderRadius }]}
activeOpacity={0.7} activeOpacity={0.7}
onPress={handlePress} onPress={handlePress}
onLongPress={handleLongPress} onLongPress={handleLongPress}
delayLongPress={300} delayLongPress={300}
// TV focus highlight: visible focus ring + scale, no dim-on-press
enableTVFocus={Platform.isTV}
focusBorderRadius={borderRadius}
focusRingColor={currentTheme.colors.primary}
focusRingWidth={3}
focusScale={getDeviceType(width) === 'tv' ? 1.08 : 1.06}
> >
<View ref={itemRef} style={[styles.contentItemContainer, { borderRadius }]}> <View ref={itemRef} style={[styles.contentItemContainer, { borderRadius }]}>
{/* Image with FastImage for aggressive caching */} {/* Image with FastImage for aggressive caching */}
@ -362,7 +369,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
</View> </View>
)} )}
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
{settings.showPosterTitles && ( {settings.showPosterTitles && (
<Text <Text
style={[ style={[

View file

@ -27,6 +27,7 @@ import { stremioService } from '../../services/stremioService';
import { streamCacheService } from '../../services/streamCacheService'; import { streamCacheService } from '../../services/streamCacheService';
import { useSettings } from '../../hooks/useSettings'; import { useSettings } from '../../hooks/useSettings';
import CustomAlert from '../../components/CustomAlert'; import CustomAlert from '../../components/CustomAlert';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
// Define interface for continue watching items // Define interface for continue watching items
interface ContinueWatchingItem extends StreamingContent { interface ContinueWatchingItem extends StreamingContent {
@ -1081,7 +1082,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Memoized render function for continue watching items // Memoized render function for continue watching items
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => ( const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.wideContentItem, styles.wideContentItem,
{ {
@ -1096,6 +1097,11 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
onPress={() => handleContentPress(item)} onPress={() => handleContentPress(item)}
onLongPress={() => handleLongPress(item)} onLongPress={() => handleLongPress(item)}
delayLongPress={800} delayLongPress={800}
enableTVFocus={Platform.isTV}
focusBorderRadius={14}
focusRingColor={currentTheme.colors.primary}
focusRingWidth={3}
focusScale={isTV ? 1.06 : 1.04}
> >
{/* Poster Image */} {/* Poster Image */}
<View style={[ <View style={[
@ -1242,7 +1248,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
</View> </View>
)} )}
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet]); ), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet]);
// Memoized key extractor // Memoized key extractor

View file

@ -5,7 +5,6 @@ import {
StyleSheet, StyleSheet,
Modal, Modal,
Pressable, Pressable,
TouchableOpacity,
useColorScheme, useColorScheme,
Dimensions, Dimensions,
Platform Platform
@ -28,6 +27,7 @@ import {
GestureHandlerRootView, GestureHandlerRootView,
} from 'react-native-gesture-handler'; } from 'react-native-gesture-handler';
import { StreamingContent } from '../../services/catalogService'; import { StreamingContent } from '../../services/catalogService';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
interface DropUpMenuProps { interface DropUpMenuProps {
visible: boolean; visible: boolean;
@ -184,7 +184,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
</View> </View>
<View style={styles.menuOptions}> <View style={styles.menuOptions}>
{menuOptions.map((option, index) => ( {menuOptions.map((option, index) => (
<TouchableOpacity <FocusableTouchableOpacity
key={option.action} key={option.action}
style={[ style={[
styles.menuOption, styles.menuOption,
@ -195,6 +195,10 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
onOptionSelect(option.action); onOptionSelect(option.action);
onClose(); onClose();
}} }}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV && visible && index === 0}
> >
<MaterialIcons <MaterialIcons
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"} name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
@ -207,7 +211,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
]}> ]}>
{option.label} {option.label}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
</Animated.View> </Animated.View>

View file

@ -30,6 +30,7 @@ import { SkeletonFeatured } from './SkeletonLoaders';
import { hasValidLogoFormat, isTmdbUrl } from '../../utils/logoUtils'; import { hasValidLogoFormat, isTmdbUrl } from '../../utils/logoUtils';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { useTheme } from '../../contexts/ThemeContext'; import { useTheme } from '../../contexts/ThemeContext';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
interface FeaturedContentProps { interface FeaturedContentProps {
featuredContent: StreamingContent | null; featuredContent: StreamingContent | null;
@ -495,7 +496,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
)} )}
<Animated.View style={[styles.tabletButtons as ViewStyle, buttonsAnimatedStyle]}> <Animated.View style={[styles.tabletButtons as ViewStyle, buttonsAnimatedStyle]}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.tabletPlayButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]} style={[styles.tabletPlayButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
onPress={() => { onPress={() => {
if (featuredContent) { if (featuredContent) {
@ -507,12 +508,17 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
}} }}
activeOpacity={0.8} activeOpacity={0.8}
hasTVPreferredFocus={Platform.isTV} hasTVPreferredFocus={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={30}
focusRingColor={currentTheme.colors.primary}
focusRingWidth={3}
focusScale={1.04}
> >
<MaterialIcons name="play-arrow" size={28} color={currentTheme.colors.black} /> <MaterialIcons name="play-arrow" size={28} color={currentTheme.colors.black} />
<Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}> <Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play Now Play Now
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.tabletSecondaryButton as ViewStyle, { backgroundColor: 'rgba(255,255,255,0.2)', borderColor: 'rgba(255,255,255,0.3)' }]} style={[styles.tabletSecondaryButton as ViewStyle, { backgroundColor: 'rgba(255,255,255,0.2)', borderColor: 'rgba(255,255,255,0.3)' }]}
@ -631,7 +637,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]} style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
onPress={() => { onPress={() => {
if (featuredContent) { if (featuredContent) {
@ -643,12 +649,17 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
}} }}
activeOpacity={0.8} activeOpacity={0.8}
hasTVPreferredFocus={Platform.isTV} hasTVPreferredFocus={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={30}
focusRingColor={currentTheme.colors.primary}
focusRingWidth={3}
focusScale={1.04}
> >
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} /> <MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}> <Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play Play
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.infoButton as ViewStyle} style={styles.infoButton as ViewStyle}

View file

@ -56,6 +56,7 @@ import { TMDBService } from '../../services/tmdbService';
import TrailerService from '../../services/trailerService'; import TrailerService from '../../services/trailerService';
import TrailerPlayer from '../video/TrailerPlayer'; import TrailerPlayer from '../video/TrailerPlayer';
import { HERO_HEIGHT, SCREEN_WIDTH as width, IS_TABLET as isTablet } from '../../constants/dimensions'; import { HERO_HEIGHT, SCREEN_WIDTH as width, IS_TABLET as isTablet } from '../../constants/dimensions';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
const { height } = Dimensions.get('window'); const { height } = Dimensions.get('window');
@ -344,7 +345,7 @@ const ActionButtons = memo(({
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}> <Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
{/* Single Row Layout - Play, Save, and optionally Collection/Ratings */} {/* Single Row Layout - Play, Save, and optionally Collection/Ratings */}
<View style={styles.singleRowLayout}> <View style={styles.singleRowLayout}>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
playButtonStyle, playButtonStyle,
isTablet && styles.tabletPlayButton, isTablet && styles.tabletPlayButton,
@ -354,6 +355,10 @@ const ActionButtons = memo(({
activeOpacity={0.85} activeOpacity={0.85}
hasTVPreferredFocus={Platform.isTV} hasTVPreferredFocus={Platform.isTV}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTablet ? 20 : 18}
focusRingWidth={3}
focusScale={1.04}
> >
<MaterialIcons <MaterialIcons
name={(() => { name={(() => {
@ -366,9 +371,9 @@ const ActionButtons = memo(({
color={isWatched && type === 'movie' ? "#fff" : "#000"} color={isWatched && type === 'movie' ? "#fff" : "#000"}
/> />
<Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text> <Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.actionButton, styles.actionButton,
styles.infoButton, styles.infoButton,
@ -378,6 +383,10 @@ const ActionButtons = memo(({
onPress={handleSaveAction} onPress={handleSaveAction}
activeOpacity={0.85} activeOpacity={0.85}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTablet ? 20 : 18}
focusRingWidth={3}
focusScale={1.04}
> >
{Platform.OS === 'ios' ? ( {Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? ( GlassViewComp && liquidGlassAvailable ? (
@ -399,15 +408,19 @@ const ActionButtons = memo(({
<Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}> <Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}>
{inLibrary ? 'Saved' : 'Save'} {inLibrary ? 'Saved' : 'Save'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
{/* Trakt Collection Button */} {/* Trakt Collection Button */}
{hasTraktCollection && ( {hasTraktCollection && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]} style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
onPress={handleCollectionAction} onPress={handleCollectionAction}
activeOpacity={0.85} activeOpacity={0.85}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={999}
focusRingWidth={3}
focusScale={1.06}
> >
{Platform.OS === 'ios' ? ( {Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? ( GlassViewComp && liquidGlassAvailable ? (
@ -426,16 +439,20 @@ const ActionButtons = memo(({
size={isTablet ? 28 : 24} size={isTablet ? 28 : 24}
color={isInCollection ? "#3498DB" : currentTheme.colors.white} color={isInCollection ? "#3498DB" : currentTheme.colors.white}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
{/* Ratings Button (for series) */} {/* Ratings Button (for series) */}
{hasRatings && ( {hasRatings && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]} style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
onPress={handleRatingsPress} onPress={handleRatingsPress}
activeOpacity={0.85} activeOpacity={0.85}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={999}
focusRingWidth={3}
focusScale={1.06}
> >
{Platform.OS === 'ios' ? ( {Platform.OS === 'ios' ? (
GlassViewComp && liquidGlassAvailable ? ( GlassViewComp && liquidGlassAvailable ? (
@ -454,7 +471,7 @@ const ActionButtons = memo(({
size={isTablet ? 28 : 24} size={isTablet ? 28 : 24}
color={currentTheme.colors.white} color={currentTheme.colors.white}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
</Animated.View> </Animated.View>
@ -1757,7 +1774,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
right: width >= 768 ? 32 : 16, right: width >= 768 ? 32 : 16,
zIndex: 1000, zIndex: 1000,
}}> }}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { onPress={() => {
// Extract episode info if it's a series // Extract episode info if it's a series
let episodeData = null; let episodeData = null;
@ -1782,6 +1799,10 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
}} }}
activeOpacity={0.7} activeOpacity={0.7}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={20}
focusRingWidth={3}
focusScale={1.06}
style={{ style={{
padding: 8, padding: 8,
backgroundColor: 'rgba(0, 0, 0, 0.5)', backgroundColor: 'rgba(0, 0, 0, 0.5)',
@ -1793,19 +1814,27 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
size={24} size={24}
color="white" color="white"
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
</Animated.View> </Animated.View>
)} )}
<Animated.View style={styles.backButtonContainer}> <Animated.View style={styles.backButtonContainer}>
<TouchableOpacity style={styles.backButton} onPress={handleBack} focusable={Platform.isTV}> <FocusableTouchableOpacity
style={styles.backButton}
onPress={handleBack}
focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={999}
focusRingWidth={3}
focusScale={1.06}
>
<MaterialIcons <MaterialIcons
name="arrow-back" name="arrow-back"
size={28} size={28}
color="#fff" color="#fff"
style={styles.backButtonIcon} style={styles.backButtonIcon}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
</Animated.View> </Animated.View>
{/* Ultra-light Gradient with subtle dynamic background blend */} {/* Ultra-light Gradient with subtle dynamic background blend */}

View file

@ -18,6 +18,7 @@ import { useTheme } from '../../contexts/ThemeContext';
import { TMDBService } from '../../services/tmdbService'; import { TMDBService } from '../../services/tmdbService';
import { catalogService } from '../../services/catalogService'; import { catalogService } from '../../services/catalogService';
import CustomAlert from '../../components/CustomAlert'; import CustomAlert from '../../components/CustomAlert';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@ -117,9 +118,15 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
}; };
const renderItem = ({ item }: { item: StreamingContent }) => ( const renderItem = ({ item }: { item: StreamingContent }) => (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.itemContainer, { width: posterWidth, marginRight: itemSpacing }]} style={[styles.itemContainer, { width: posterWidth, marginRight: itemSpacing }]}
onPress={() => handleItemPress(item)} onPress={() => 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}
> >
<FastImage <FastImage
source={{ uri: item.poster }} source={{ uri: item.poster }}
@ -129,7 +136,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
<Text style={[styles.title, { color: currentTheme.colors.mediumEmphasis, fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13, lineHeight: isTV ? 20 : 18 }]} numberOfLines={2}> <Text style={[styles.title, { color: currentTheme.colors.mediumEmphasis, fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13, lineHeight: isTV ? 20 : 18 }]} numberOfLines={2}>
{item.name} {item.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
if (loadingRecommendations) { if (loadingRecommendations) {

View file

@ -16,6 +16,7 @@ import { TraktService } from '../../services/traktService';
import { watchedService } from '../../services/watchedService'; import { watchedService } from '../../services/watchedService';
import { logger } from '../../utils/logger'; import { logger } from '../../utils/logger';
import { mmkvStorage } from '../../services/mmkvStorage'; import { mmkvStorage } from '../../services/mmkvStorage';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
// Enhanced responsive breakpoints for Seasons Section // Enhanced responsive breakpoints for Seasons Section
const BREAKPOINTS = { const BREAKPOINTS = {
@ -779,7 +780,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
]}>Seasons</Text> ]}>Seasons</Text>
{/* Dropdown Toggle Button */} {/* Dropdown Toggle Button */}
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.seasonViewToggle, styles.seasonViewToggle,
{ {
@ -796,6 +797,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
]} ]}
activeOpacity={0.7} activeOpacity={0.7}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTV ? 10 : isLargeTablet ? 8 : isTablet ? 6 : 6}
focusRingWidth={3}
focusScale={1.03}
> >
<Text style={[ <Text style={[
styles.seasonViewToggleText, styles.seasonViewToggleText,
@ -808,7 +813,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
]}> ]}>
{seasonViewMode === 'posters' ? 'Posters' : 'Text'} {seasonViewMode === 'posters' ? 'Posters' : 'Text'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<FlatList <FlatList
@ -846,7 +851,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
key={season} key={season}
style={{ opacity: textViewVisible ? 1 : 0 }} style={{ opacity: textViewVisible ? 1 : 0 }}
> >
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.seasonTextButton, styles.seasonTextButton,
{ {
@ -860,6 +865,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
]} ]}
onPress={() => onSeasonChange(season)} onPress={() => onSeasonChange(season)}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12}
focusRingWidth={3}
focusScale={1.03}
> >
<Text style={[ <Text style={[
styles.seasonTextButtonText, styles.seasonTextButtonText,
@ -873,7 +882,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
]} numberOfLines={1}> ]} numberOfLines={1}>
{season === 0 ? 'Specials' : `Season ${season}`} {season === 0 ? 'Specials' : `Season ${season}`}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
} }
@ -885,7 +894,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
key={season} key={season}
style={{ opacity: posterViewVisible ? 1 : 0 }} style={{ opacity: posterViewVisible ? 1 : 0 }}
> >
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.seasonButton, styles.seasonButton,
{ {
@ -896,6 +905,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
]} ]}
onPress={() => onSeasonChange(season)} onPress={() => onSeasonChange(season)}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8}
focusRingWidth={3}
focusScale={1.03}
> >
<View style={[ <View style={[
styles.seasonPosterContainer, styles.seasonPosterContainer,
@ -937,7 +950,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
> >
{season === 0 ? 'Specials' : `Season ${season}`} {season === 0 ? 'Specials' : `Season ${season}`}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
}} }}
@ -1022,7 +1035,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
const showProgress = progress && progressPercent < 85; const showProgress = progress && progressPercent < 85;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={episode.id} key={episode.id}
style={[ style={[
styles.episodeCardVertical, styles.episodeCardVertical,
@ -1038,6 +1051,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
delayLongPress={400} delayLongPress={400}
activeOpacity={0.7} activeOpacity={0.7}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
focusRingWidth={3}
focusScale={1.02}
> >
<View style={[ <View style={[
styles.episodeImageContainer, styles.episodeImageContainer,
@ -1228,7 +1245,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
{(episode.overview || (episode as any).description || (episode as any).plot || (episode as any).synopsis || 'No description available')} {(episode.overview || (episode as any).description || (episode as any).plot || (episode as any).synopsis || 'No description available')}
</Text> </Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };

View file

@ -20,6 +20,7 @@ import { logger } from '../../utils/logger';
import TrailerService from '../../services/trailerService'; import TrailerService from '../../services/trailerService';
import TrailerModal from './TrailerModal'; import TrailerModal from './TrailerModal';
import Animated, { useSharedValue, withTiming, withDelay, useAnimatedStyle } from 'react-native-reanimated'; import Animated, { useSharedValue, withTiming, withDelay, useAnimatedStyle } from 'react-native-reanimated';
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
// Enhanced responsive breakpoints for Trailers Section // Enhanced responsive breakpoints for Trailers Section
const BREAKPOINTS = { const BREAKPOINTS = {
@ -517,7 +518,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
{/* Category Selector - Right Aligned */} {/* Category Selector - Right Aligned */}
{trailerCategories.length > 0 && selectedCategory && ( {trailerCategories.length > 0 && selectedCategory && (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.categorySelector, styles.categorySelector,
{ {
@ -531,6 +532,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
onPress={toggleDropdown} onPress={toggleDropdown}
activeOpacity={0.8} activeOpacity={0.8}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
focusRingWidth={3}
focusScale={1.03}
> >
<Text <Text
style={[ style={[
@ -551,7 +556,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
size={isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18} size={isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18}
color="rgba(255,255,255,0.7)" color="rgba(255,255,255,0.7)"
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
@ -575,7 +580,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16 borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
}]}> }]}>
{trailerCategories.map(category => ( {trailerCategories.map(category => (
<TouchableOpacity <FocusableTouchableOpacity
key={category} key={category}
style={[ style={[
styles.dropdownItem, styles.dropdownItem,
@ -587,6 +592,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
onPress={() => handleCategorySelect(category)} onPress={() => handleCategorySelect(category)}
activeOpacity={0.7} activeOpacity={0.7}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTV ? 18 : isLargeTablet ? 16 : isTablet ? 14 : 14}
focusRingWidth={3}
focusScale={1.02}
> >
<View style={styles.dropdownItemContent}> <View style={styles.dropdownItemContent}>
<View style={[ <View style={[
@ -626,7 +635,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
{trailers[category].length} {trailers[category].length}
</Text> </Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
@ -656,7 +665,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
{ width: trailerCardWidth } { width: trailerCardWidth }
]} ]}
> >
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.trailerCard, styles.trailerCard,
{ {
@ -667,6 +676,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
onPress={() => handleTrailerPress(trailer)} onPress={() => handleTrailerPress(trailer)}
activeOpacity={0.9} activeOpacity={0.9}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
focusRingWidth={3}
focusScale={1.03}
> >
{/* Thumbnail with Gradient Overlay */} {/* Thumbnail with Gradient Overlay */}
<View style={styles.thumbnailWrapper}> <View style={styles.thumbnailWrapper}>
@ -688,7 +701,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
} }
]} /> ]} />
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
{/* Trailer Info Below Card */} {/* Trailer Info Below Card */}
<View style={styles.trailerInfoBelow}> <View style={styles.trailerInfoBelow}>

View file

@ -1,5 +1,5 @@
import React from 'react'; 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 { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -9,6 +9,7 @@ import Animated, {
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils'; import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface AudioTrackModalProps { interface AudioTrackModalProps {
showAudioModal: boolean; showAudioModal: boolean;
@ -38,17 +39,17 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
return ( return (
<View style={StyleSheet.absoluteFill} zIndex={9999}> <View style={StyleSheet.absoluteFill} zIndex={9999}>
{/* Backdrop matching SubtitleModal */} {/* Backdrop matching SubtitleModal */}
<TouchableOpacity <Pressable
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
activeOpacity={1}
onPress={handleClose} onPress={handleClose}
focusable={false}
> >
<Animated.View <Animated.View
entering={FadeIn.duration(200)} entering={FadeIn.duration(200)}
exiting={FadeOut.duration(150)} exiting={FadeOut.duration(150)}
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }}
/> />
</TouchableOpacity> </Pressable>
{/* Center Alignment Container */} {/* Center Alignment Container */}
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none"> <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none">
@ -79,7 +80,7 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
const isSelected = selectedAudioTrack === track.id; const isSelected = selectedAudioTrack === track.id;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={track.id} key={track.id}
onPress={() => { onPress={() => {
selectAudioTrack(track.id); selectAudioTrack(track.id);
@ -93,6 +94,10 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center'
}} }}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && isSelected}
> >
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ <Text style={{
@ -104,7 +109,7 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
</Text> </Text>
</View> </View>
{isSelected && <MaterialIcons name="check" size={18} color="black" />} {isSelected && <MaterialIcons name="check" size={18} color="black" />}
</TouchableOpacity> </FocusableTouchableOpacity>
); );
})} })}

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -11,6 +11,7 @@ import { Episode } from '../../../types/metadata';
import { Stream } from '../../../types/streams'; import { Stream } from '../../../types/streams';
import { stremioService } from '../../../services/stremioService'; import { stremioService } from '../../../services/stremioService';
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface EpisodeStreamsModalProps { interface EpisodeStreamsModalProps {
visible: boolean; visible: boolean;
@ -142,17 +143,17 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
return ( return (
<View style={StyleSheet.absoluteFill} zIndex={10000}> <View style={StyleSheet.absoluteFill} zIndex={10000}>
{/* Backdrop */} {/* Backdrop */}
<TouchableOpacity <Pressable
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
activeOpacity={1}
onPress={onClose} onPress={onClose}
focusable={false}
> >
<Animated.View <Animated.View
entering={FadeIn.duration(200)} entering={FadeIn.duration(200)}
exiting={FadeOut.duration(150)} exiting={FadeOut.duration(150)}
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }}
/> />
</TouchableOpacity> </Pressable>
<Animated.View <Animated.View
entering={SlideInRight.duration(300)} entering={SlideInRight.duration(300)}
@ -218,7 +219,7 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
const quality = getQualityFromTitle(stream.title) || stream.quality; const quality = getQualityFromTitle(stream.title) || stream.quality;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={`${providerId}-${index}`} key={`${providerId}-${index}`}
style={{ style={{
padding: 8, padding: 8,
@ -232,6 +233,10 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
onClose(); onClose();
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && index === 0}
> >
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
@ -248,7 +253,7 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
)} )}
</View> </View>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
})} })}
</View> </View>

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -12,6 +12,7 @@ import { EpisodeCard } from '../cards/EpisodeCard';
import { storageService } from '../../../services/storageService'; import { storageService } from '../../../services/storageService';
import { TraktService } from '../../../services/traktService'; import { TraktService } from '../../../services/traktService';
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface EpisodesModalProps { interface EpisodesModalProps {
showEpisodesModal: boolean; showEpisodesModal: boolean;
@ -97,9 +98,9 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
return ( return (
<View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
<TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={() => setShowEpisodesModal(false)}> <Pressable style={StyleSheet.absoluteFill} onPress={() => setShowEpisodesModal(false)} focusable={false}>
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} /> <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
</TouchableOpacity> </Pressable>
<Animated.View <Animated.View
entering={SlideInRight.duration(300)} entering={SlideInRight.duration(300)}
@ -127,7 +128,7 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
if (b === 0) return -1; if (b === 0) return -1;
return a - b; return a - b;
}).map((season) => ( }).map((season) => (
<TouchableOpacity <FocusableTouchableOpacity
key={season} key={season}
onPress={() => setSelectedSeason(season)} onPress={() => setSelectedSeason(season)}
style={{ style={{
@ -138,6 +139,10 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
borderWidth: 1, borderWidth: 1,
borderColor: selectedSeason === season ? 'white' : 'rgba(255,255,255,0.1)', borderColor: selectedSeason === season ? 'white' : 'rgba(255,255,255,0.1)',
}} }}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={20}
hasTVPreferredFocus={Platform.isTV && selectedSeason === season}
> >
<Text style={{ <Text style={{
color: selectedSeason === season ? 'black' : 'white', color: selectedSeason === season ? 'black' : 'white',
@ -145,7 +150,7 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
}}> }}>
{season === 0 ? 'Specials' : `Season ${season}`} {season === 0 ? 'Specials' : `Season ${season}`}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</ScrollView> </ScrollView>
</View> </View>

View file

@ -1,5 +1,5 @@
import React from 'react'; 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 { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -7,6 +7,7 @@ import Animated, {
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
// Check if running on TV platform // Check if running on TV platform
const isTV = Platform.isTV; const isTV = Platform.isTV;
@ -58,9 +59,9 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
return ( return (
<View style={[StyleSheet.absoluteFill, { zIndex: 99999, justifyContent: 'center', alignItems: 'center' }]}> <View style={[StyleSheet.absoluteFill, { zIndex: 99999, justifyContent: 'center', alignItems: 'center' }]}>
<TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleClose}> <Pressable style={StyleSheet.absoluteFill} onPress={handleClose} focusable={false}>
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.7)' }} /> <Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.7)' }} />
</TouchableOpacity> </Pressable>
<Animated.View <Animated.View
entering={FadeIn.duration(300)} entering={FadeIn.duration(300)}
@ -111,29 +112,35 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
{errorDetails || 'An unknown error occurred during playback.'} {errorDetails || 'An unknown error occurred during playback.'}
</Text> </Text>
<TouchableOpacity {!!ExpoClipboard && (
onPress={handleCopy} <FocusableTouchableOpacity
style={{ onPress={handleCopy}
flexDirection: 'row', activeOpacity={0.9}
alignItems: 'center', enableTVFocus={Platform.isTV}
justifyContent: 'center', preset="pill"
padding: 8, focusBorderRadius={12}
marginBottom: 24, style={{
opacity: 0.8 flexDirection: 'row',
}} alignItems: 'center',
> justifyContent: 'center',
<MaterialIcons padding: 8,
name={copied ? "check" : "content-copy"} marginBottom: 24,
size={16} opacity: 0.9
color="rgba(255,255,255,0.6)" }}
style={{ marginRight: 6 }} >
/> <MaterialIcons
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontWeight: '500' }}> name={copied ? "check" : "content-copy"}
{copied ? 'Copied to clipboard' : 'Copy error details'} size={16}
</Text> color="rgba(255,255,255,0.6)"
</TouchableOpacity> style={{ marginRight: 6 }}
/>
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontWeight: '500' }}>
{copied ? 'Copied to clipboard' : 'Copy error details'}
</Text>
</FocusableTouchableOpacity>
)}
<TouchableOpacity <FocusableTouchableOpacity
style={{ style={{
backgroundColor: 'white', backgroundColor: 'white',
paddingVertical: 12, paddingVertical: 12,
@ -144,6 +151,10 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
}} }}
onPress={handleClose} onPress={handleClose}
activeOpacity={0.9} activeOpacity={0.9}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV}
> >
<Text style={{ <Text style={{
color: 'black', color: 'black',
@ -152,7 +163,7 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
}}> }}>
Dismiss Dismiss
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</Animated.View> </Animated.View>
</View> </View>
); );

View file

@ -1,5 +1,5 @@
import React, { useEffect } from 'react'; 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 { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import Reanimated, { import Reanimated, {
@ -12,6 +12,7 @@ import Reanimated, {
withDelay withDelay
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { styles } from '../utils/playerStyles'; import { styles } from '../utils/playerStyles';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface LoadingOverlayProps { interface LoadingOverlayProps {
visible: boolean; visible: boolean;
@ -118,13 +119,17 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
/> />
<TouchableOpacity <FocusableTouchableOpacity
style={styles.loadingCloseButton} style={styles.loadingCloseButton}
onPress={onClose} onPress={onClose}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={999}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="close" size={24} color="#ffffff" /> <MaterialIcons name="close" size={24} color="#ffffff" />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.openingContent}> <View style={styles.openingContent}>
{hasLogo && logo ? ( {hasLogo && logo ? (

View file

@ -1,10 +1,11 @@
import React, { useEffect } from 'react'; 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 { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { styles } from '../utils/playerStyles'; import { styles } from '../utils/playerStyles';
import { formatTime } from '../utils/playerUtils'; import { formatTime } from '../utils/playerUtils';
import { logger } from '../../../utils/logger'; import { logger } from '../../../utils/logger';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface ResumeOverlayProps { interface ResumeOverlayProps {
showResumeOverlay: boolean; showResumeOverlay: boolean;
@ -71,20 +72,27 @@ export const ResumeOverlay: React.FC<ResumeOverlayProps> = ({
</View> </View>
<View style={styles.resumeButtons}> <View style={styles.resumeButtons}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.resumeButton} style={styles.resumeButton}
onPress={handleStartFromBeginning} onPress={handleStartFromBeginning}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={12}
> >
<Ionicons name="refresh" size={16} color="white" style={styles.buttonIcon} /> <Ionicons name="refresh" size={16} color="white" style={styles.buttonIcon} />
<Text style={styles.resumeButtonText}>Start Over</Text> <Text style={styles.resumeButtonText}>Start Over</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.resumeButton, styles.resumeFromButton]} style={[styles.resumeButton, styles.resumeFromButton]}
onPress={handleResume} onPress={handleResume}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV}
> >
<Ionicons name="play" size={16} color="white" style={styles.buttonIcon} /> <Ionicons name="play" size={16} color="white" style={styles.buttonIcon} />
<Text style={styles.resumeButtonText}>Resume</Text> <Text style={styles.resumeButtonText}>Resume</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</LinearGradient> </LinearGradient>
</View> </View>

View file

@ -8,6 +8,7 @@ import Animated, {
SlideOutRight, SlideOutRight,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { Stream } from '../../../types/streams'; import { Stream } from '../../../types/streams';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface SourcesModalProps { interface SourcesModalProps {
showSourcesModal: boolean; showSourcesModal: boolean;
@ -168,7 +169,7 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
const quality = getQualityFromTitle(stream.title) || stream.quality; const quality = getQualityFromTitle(stream.title) || stream.quality;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={`${providerId}-${index}`} key={`${providerId}-${index}`}
style={{ style={{
padding: 8, padding: 8,
@ -181,6 +182,10 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
onPress={() => handleStreamSelect(stream)} onPress={() => handleStreamSelect(stream)}
activeOpacity={0.7} activeOpacity={0.7}
disabled={isChangingSource === true} disabled={isChangingSource === true}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && (isSelected || (providerId === sortedProviders[0]?.[0] && index === 0))}
> >
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
@ -227,7 +232,7 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
)} )}
</View> </View>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
})} })}
</View> </View>

View file

@ -1,5 +1,5 @@
import React from 'react'; 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 { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -9,6 +9,7 @@ import Animated, {
useAnimatedStyle, useAnimatedStyle,
withTiming, withTiming,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface SpeedModalProps { interface SpeedModalProps {
showSpeedModal: boolean; showSpeedModal: boolean;
@ -31,7 +32,15 @@ const MorphingButton = ({ label, isSelected, onPress, isSmall = false }: any) =>
}); });
return ( return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8} style={{ flex: isSmall ? 0 : 1 }}> <FocusableTouchableOpacity
onPress={onPress}
activeOpacity={0.8}
style={{ flex: isSmall ? 0 : 1 }}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={10}
hasTVPreferredFocus={Platform.isTV && isSelected}
>
<Animated.View style={[{ paddingVertical: isSmall ? 6 : 8, paddingHorizontal: isSmall ? 14 : 0, alignItems: 'center', justifyContent: 'center' }, animatedStyle]}> <Animated.View style={[{ paddingVertical: isSmall ? 6 : 8, paddingHorizontal: isSmall ? 14 : 0, alignItems: 'center', justifyContent: 'center' }, animatedStyle]}>
<Text style={{ <Text style={{
color: isSelected && !isSmall ? 'black' : 'white', color: isSelected && !isSmall ? 'black' : 'white',
@ -41,7 +50,7 @@ const MorphingButton = ({ label, isSelected, onPress, isSmall = false }: any) =>
{label} {label}
</Text> </Text>
</Animated.View> </Animated.View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -62,14 +71,14 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
if (!showSpeedModal) return null; if (!showSpeedModal) return null;
return ( return (
<View style={StyleSheet.absoluteFill} zIndex={9999}> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
<TouchableOpacity <Pressable
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
activeOpacity={1}
onPress={() => setShowSpeedModal(false)} onPress={() => setShowSpeedModal(false)}
focusable={false}
> >
<Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.2)' }} /> <Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.2)' }} />
</TouchableOpacity> </Pressable>
<View pointerEvents="box-none" style={{ ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', paddingBottom: 20 }}> <View pointerEvents="box-none" style={{ ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', paddingBottom: 20 }}>
<Animated.View <Animated.View
@ -104,9 +113,13 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
{/* On Hold Section */} {/* On Hold Section */}
<View> <View>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => setHoldToSpeedEnabled(!holdToSpeedEnabled)} onPress={() => setHoldToSpeedEnabled(!holdToSpeedEnabled)}
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: holdToSpeedEnabled ? 15 : 0 }} style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: holdToSpeedEnabled ? 15 : 0 }}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV}
> >
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14 }}>On Hold</Text> <Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14 }}>On Hold</Text>
<View style={{ <View style={{
@ -116,7 +129,7 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
}}> }}>
<View style={{ width: 14, height: 14, borderRadius: 7, backgroundColor: holdToSpeedEnabled ? 'black' : 'white' }} /> <View style={{ width: 14, height: 14, borderRadius: 7, backgroundColor: holdToSpeedEnabled ? 'black' : 'white' }} />
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
{holdToSpeedEnabled && ( {holdToSpeedEnabled && (
<Animated.View entering={FadeIn} style={{ flexDirection: 'row', gap: 8 }}> <Animated.View entering={FadeIn} style={{ flexDirection: 'row', gap: 8 }}>

View file

@ -1,5 +1,5 @@
import React from 'react'; 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 { MaterialIcons } from '@expo/vector-icons';
import Animated, { import Animated, {
FadeIn, FadeIn,
@ -11,6 +11,7 @@ import Animated, {
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes'; import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils'; import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
interface SubtitleModalsProps { interface SubtitleModalsProps {
showSubtitleModal: boolean; showSubtitleModal: boolean;
@ -65,13 +66,21 @@ const MorphingTab = ({ label, isSelected, onPress }: any) => {
})); }));
return ( return (
<TouchableOpacity onPress={onPress} activeOpacity={0.8} style={{ flex: 1 }}> <FocusableTouchableOpacity
onPress={onPress}
activeOpacity={0.8}
style={{ flex: 1 }}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && isSelected}
>
<Animated.View style={[{ paddingVertical: 8, alignItems: 'center', justifyContent: 'center' }, animatedStyle]}> <Animated.View style={[{ paddingVertical: 8, alignItems: 'center', justifyContent: 'center' }, animatedStyle]}>
<Text style={{ color: isSelected ? 'black' : 'white', fontWeight: isSelected ? '700' : '400', fontSize: 13 }}> <Text style={{ color: isSelected ? 'black' : 'white', fontWeight: isSelected ? '700' : '400', fontSize: 13 }}>
{label} {label}
</Text> </Text>
</Animated.View> </Animated.View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -112,11 +121,11 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
if (!showSubtitleModal) return null; if (!showSubtitleModal) return null;
return ( return (
<View style={StyleSheet.absoluteFill} zIndex={9999}> <View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
{/* Backdrop */} {/* Backdrop */}
<TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleClose}> <Pressable style={StyleSheet.absoluteFill} onPress={handleClose} focusable={false}>
<Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} /> <Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
</TouchableOpacity> </Pressable>
{/* Centered Modal Container */} {/* Centered Modal Container */}
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none"> <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none">
@ -149,21 +158,29 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
<View style={{ paddingHorizontal: 20, paddingBottom: 20 }}> <View style={{ paddingHorizontal: 20, paddingBottom: 20 }}>
{activeTab === 'built-in' && ( {activeTab === 'built-in' && (
<View style={{ gap: 8 }}> <View style={{ gap: 8 }}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }} onPress={() => { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }}
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }} 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}
> >
<Text style={{ color: selectedTextTrack === -1 ? 'black' : 'rgba(96, 20, 16)', fontWeight: '600' }}>None</Text> <Text style={{ color: selectedTextTrack === -1 ? 'black' : 'rgba(96, 20, 16)', fontWeight: '600' }}>None</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
{ksTextTracks.map((track) => ( {ksTextTracks.map((track) => (
<TouchableOpacity <FocusableTouchableOpacity
key={track.id} key={track.id}
onPress={() => { selectTextTrack(track.id); setSelectedOnlineSubtitleId(null); }} onPress={() => { 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' }} 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}
> >
<Text style={{ color: selectedTextTrack === track.id ? 'black' : 'white' }}>{getTrackDisplayName(track)}</Text> <Text style={{ color: selectedTextTrack === track.id ? 'black' : 'white' }}>{getTrackDisplayName(track)}</Text>
{selectedTextTrack === track.id && <MaterialIcons name="check" size={18} color="black" />} {selectedTextTrack === track.id && <MaterialIcons name="check" size={18} color="black" />}
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
)} )}
@ -171,23 +188,34 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
{activeTab === 'addon' && ( {activeTab === 'addon' && (
<View style={{ gap: 8 }}> <View style={{ gap: 8 }}>
{availableSubtitles.length === 0 ? ( {availableSubtitles.length === 0 ? (
<TouchableOpacity onPress={fetchAvailableSubtitles} style={{ padding: 40, alignItems: 'center', opacity: 0.5 }}> <FocusableTouchableOpacity
onPress={fetchAvailableSubtitles}
style={{ padding: 40, alignItems: 'center', opacity: 0.8 }}
enableTVFocus={Platform.isTV}
preset="card"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV}
>
<MaterialIcons name="cloud-download" size={32} color="white" /> <MaterialIcons name="cloud-download" size={32} color="white" />
<Text style={{ color: 'white', marginTop: 10 }}>Search Online Subtitles</Text> <Text style={{ color: 'white', marginTop: 10 }}>Search Online Subtitles</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
) : ( ) : (
availableSubtitles.map((sub) => ( availableSubtitles.map((sub) => (
<TouchableOpacity <FocusableTouchableOpacity
key={sub.id} key={sub.id}
onPress={() => { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }} onPress={() => { 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}
> >
<View> <View>
<Text style={{ marginLeft: 5, color: selectedOnlineSubtitleId === sub.id ? 'black' : 'white', fontWeight: '600' }}>{sub.display}</Text> <Text style={{ marginLeft: 5, color: selectedOnlineSubtitleId === sub.id ? 'black' : 'white', fontWeight: '600' }}>{sub.display}</Text>
<Text style={{ marginLeft: 5, color: selectedOnlineSubtitleId === sub.id ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)', fontSize: 11, paddingBottom: 3 }}>{formatLanguage(sub.language)}</Text> <Text style={{ marginLeft: 5, color: selectedOnlineSubtitleId === sub.id ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)', fontSize: 11, paddingBottom: 3 }}>{formatLanguage(sub.language)}</Text>
</View> </View>
{selectedOnlineSubtitleId === sub.id && <MaterialIcons name="check" size={18} color="black" />} {selectedOnlineSubtitleId === sub.id && <MaterialIcons name="check" size={18} color="black" />}
</TouchableOpacity> </FocusableTouchableOpacity>
)) ))
)} )}
</View> </View>
@ -233,7 +261,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>Quick Presets</Text> <Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>Quick Presets</Text>
</View> </View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}> <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true); setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
@ -241,33 +269,45 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
setSubtitleLineHeightMultiplier(1.2); 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)' }} 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}
> >
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 11 : 12 }}>Default</Text> <Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 11 : 12 }}>Default</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { onPress={() => {
setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false); 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)' }} 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}
> >
<Text style={{ color: '#FFD700', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Yellow</Text> <Text style={{ color: '#FFD700', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Yellow</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5); 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)' }} 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}
> >
<Text style={{ color: '#22C55E', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>High Contrast</Text> <Text style={{ color: '#22C55E', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>High Contrast</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3); 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)' }} 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}
> >
<Text style={{ color: '#3B82F6', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Large</Text> <Text style={{ color: '#3B82F6', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Large</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
@ -283,15 +323,27 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Font Size</Text> <Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Font Size</Text>
</View> </View>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}> <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<TouchableOpacity onPress={decreaseSubtitleSize} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', justifyContent: 'center', alignItems: 'center' }}> <FocusableTouchableOpacity
onPress={decreaseSubtitleSize}
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', justifyContent: 'center', alignItems: 'center' }}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={controlBtn.radius}
>
<MaterialIcons name="remove" size={18} color="#FFFFFF" /> <MaterialIcons name="remove" size={18} color="#FFFFFF" />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ minWidth: 42, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 42, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: '#fff', textAlign: 'center', fontWeight: '700' }}>{subtitleSize}</Text> <Text style={{ color: '#fff', textAlign: 'center', fontWeight: '700' }}>{subtitleSize}</Text>
</View> </View>
<TouchableOpacity onPress={increaseSubtitleSize} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', justifyContent: 'center', alignItems: 'center' }}> <FocusableTouchableOpacity
onPress={increaseSubtitleSize}
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', justifyContent: 'center', alignItems: 'center' }}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={controlBtn.radius}
>
<MaterialIcons name="add" size={18} color="#FFFFFF" /> <MaterialIcons name="add" size={18} color="#FFFFFF" />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
@ -299,12 +351,15 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
<MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" /> <MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" />
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Show Background</Text> <Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Show Background</Text>
</View> </View>
<TouchableOpacity <FocusableTouchableOpacity
style={{ width: isCompact ? 48 : 54, height: isCompact ? 28 : 30, backgroundColor: subtitleBackground ? 'white' : 'rgba(255,255,255,0.25)', borderRadius: 15, justifyContent: 'center', alignItems: subtitleBackground ? 'flex-end' : 'flex-start', paddingHorizontal: 3 }} style={{ width: isCompact ? 48 : 54, height: isCompact ? 28 : 30, backgroundColor: subtitleBackground ? 'white' : 'rgba(255,255,255,0.25)', borderRadius: 15, justifyContent: 'center', alignItems: subtitleBackground ? 'flex-end' : 'flex-start', paddingHorizontal: 3 }}
onPress={toggleSubtitleBackground} onPress={toggleSubtitleBackground}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={15}
> >
<View style={{ width: 24, height: 24, backgroundColor: subtitleBackground ? 'black' : 'white', borderRadius: 12 }} /> <View style={{ width: 24, height: 24, backgroundColor: subtitleBackground ? 'black' : 'white', borderRadius: 12 }} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
@ -321,7 +376,14 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
</View> </View>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}> <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => ( {['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> <FocusableTouchableOpacity
key={c}
onPress={() => 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}
/>
))} ))}
</View> </View>
</View> </View>
@ -329,95 +391,175 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
<Text style={{ color: 'white', fontWeight: '600' }}>Align</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Align</Text>
<View style={{ flexDirection: 'row', gap: 8 }}> <View style={{ flexDirection: 'row', gap: 8 }}>
{([ { key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' } ] as const).map(a => ( {([ { key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' } ] as const).map(a => (
<TouchableOpacity key={a.key} onPress={() => 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)' }}> <FocusableTouchableOpacity
key={a.key}
onPress={() => 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}
>
<MaterialIcons name={a.icon as any} size={18} color="#FFFFFF" /> <MaterialIcons name={a.icon as any} size={18} color="#FFFFFF" />
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
</View> </View>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Bottom Offset</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Bottom Offset</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}> <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="keyboard-arrow-down" color="#fff" size={20} /> <MaterialIcons name="keyboard-arrow-down" color="#fff" size={20} />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ minWidth: 46, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 46, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBottomOffset}</Text> <Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBottomOffset}</Text>
</View> </View>
<TouchableOpacity onPress={() => setSubtitleBottomOffset(subtitleBottomOffset + 5)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="keyboard-arrow-up" color="#fff" size={20} /> <MaterialIcons name="keyboard-arrow-up" color="#fff" size={20} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}> <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="remove" color="#fff" size={18} /> <MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBgOpacity.toFixed(1)}</Text> <Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBgOpacity.toFixed(1)}</Text>
</View> </View>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="add" color="#fff" size={18} /> <MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Text Shadow</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Text Shadow</Text>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<Text style={{ color: '#fff', fontWeight: '700' }}>{subtitleTextShadow ? 'On' : 'Off'}</Text> <Text style={{ color: '#fff', fontWeight: '700' }}>{subtitleTextShadow ? 'On' : 'Off'}</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white' }}>Outline Color</Text> <Text style={{ color: 'white' }}>Outline Color</Text>
<View style={{ flexDirection: 'row', gap: 8 }}> <View style={{ flexDirection: 'row', gap: 8 }}>
{['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => ( {['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => (
<TouchableOpacity key={c} onPress={() => setSubtitleOutlineColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} /> <FocusableTouchableOpacity
key={c}
onPress={() => 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}
/>
))} ))}
</View> </View>
</View> </View>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white' }}>Outline Width</Text> <Text style={{ color: 'white' }}>Outline Width</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}> <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="remove" color="#fff" size={18} /> <MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ minWidth: 42, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 42, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOutlineWidth}</Text> <Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOutlineWidth}</Text>
</View> </View>
<TouchableOpacity onPress={() => setSubtitleOutlineWidth(subtitleOutlineWidth + 1)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="add" color="#fff" size={18} /> <MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
<View style={{ flexDirection: isCompact ? 'column' : 'row', justifyContent: 'space-between', gap: 12 }}> <View style={{ flexDirection: isCompact ? 'column' : 'row', justifyContent: 'space-between', gap: 12 }}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Letter Spacing</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Letter Spacing</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="remove" color="#fff" size={18} /> <MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleLetterSpacing.toFixed(1)}</Text> <Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleLetterSpacing.toFixed(1)}</Text>
</View> </View>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="add" color="#fff" size={18} /> <MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Line Height</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Line Height</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="remove" color="#fff" size={18} /> <MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleLineHeightMultiplier.toFixed(1)}</Text> <Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleLineHeightMultiplier.toFixed(1)}</Text>
</View> </View>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="add" color="#fff" size={18} /> <MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
</View> </View>
@ -425,21 +567,33 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}> <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text> <Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text>
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}> <View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="remove" color="#fff" size={18} /> <MaterialIcons name="remove" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ minWidth: 60, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}> <View style={{ minWidth: 60, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOffsetSec.toFixed(1)}</Text> <Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOffsetSec.toFixed(1)}</Text>
</View> </View>
<TouchableOpacity onPress={() => 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' }}> <FocusableTouchableOpacity
onPress={() => 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}
>
<MaterialIcons name="add" color="#fff" size={18} /> <MaterialIcons name="add" color="#fff" size={18} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 6 }}>Nudge subtitles earlier (-) or later (+) to sync if needed.</Text> <Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 6 }}>Nudge subtitles earlier (-) or later (+) to sync if needed.</Text>
</View> </View>
<View style={{ alignItems: 'flex-end', marginTop: 8 }}> <View style={{ alignItems: 'flex-end', marginTop: 8 }}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { onPress={() => {
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true); setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
@ -447,9 +601,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
setSubtitleLineHeightMultiplier(1.2); setSubtitleOffsetSec(0); 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)' }} 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}
> >
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 12 : 14 }}>Reset to defaults</Text> <Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 12 : 14 }}>Reset to defaults</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
</View> </View>

View file

@ -4,7 +4,6 @@ import {
Text, Text,
StyleSheet, StyleSheet,
TextInput, TextInput,
TouchableOpacity,
ScrollView, ScrollView,
StatusBar, StatusBar,
KeyboardAvoidingView, KeyboardAvoidingView,
@ -58,6 +57,7 @@ import Animated, {
Extrapolate, Extrapolate,
runOnJS runOnJS
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
const isTablet = width >= 768; const isTablet = width >= 768;
@ -302,7 +302,7 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.suggestionChip, { backgroundColor: currentTheme.colors.elevation1 }]} style={[styles.suggestionChip, { backgroundColor: currentTheme.colors.elevation1 }]}
onPress={onPress} onPress={onPress}
activeOpacity={0.7} activeOpacity={0.7}
@ -310,7 +310,7 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
<Text style={[styles.suggestionText, { color: currentTheme.colors.primary }]}> <Text style={[styles.suggestionText, { color: currentTheme.colors.primary }]}>
{text} {text}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress); }, (prev, next) => prev.text === next.text && prev.onPress === next.onPress);
@ -684,7 +684,7 @@ const AIChatScreen: React.FC = () => {
headerAnimatedStyle headerAnimatedStyle
]}> ]}>
<View style={styles.headerContent}> <View style={styles.headerContent}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { onPress={() => {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => { modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => {
@ -697,7 +697,7 @@ const AIChatScreen: React.FC = () => {
style={styles.backButton} style={styles.backButton}
> >
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} /> <MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.headerInfo}> <View style={styles.headerInfo}>
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
@ -821,7 +821,7 @@ const AIChatScreen: React.FC = () => {
blurOnSubmit={false} blurOnSubmit={false}
/> />
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.sendButton, styles.sendButton,
{ {
@ -837,7 +837,7 @@ const AIChatScreen: React.FC = () => {
size={20} size={20}
color={inputText.trim() ? 'white' : currentTheme.colors.mediumEmphasis} color={inputText.trim() ? 'white' : currentTheme.colors.mediumEmphasis}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</Animated.View> </Animated.View>
</SafeAreaView> </SafeAreaView>

View file

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; 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 FastImage from '@d11/react-native-fast-image';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
@ -9,6 +9,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const AccountManageScreen: React.FC = () => { const AccountManageScreen: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
@ -97,9 +98,9 @@ const AccountManageScreen: React.FC = () => {
colors={[currentTheme.colors.darkBackground, '#111318']} colors={[currentTheme.colors.darkBackground, '#111318']}
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
/> />
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.headerBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}> <FocusableTouchableOpacity onPress={() => navigation.goBack()} style={styles.headerBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} /> <MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
</TouchableOpacity> </FocusableTouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Account</Text> <Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Account</Text>
<View style={{ width: 22, height: 22 }} /> <View style={{ width: 22, height: 22 }} />
</Animated.View> </Animated.View>
@ -185,7 +186,7 @@ const AccountManageScreen: React.FC = () => {
</View> </View>
{/* Save and Sign out */} {/* Save and Sign out */}
<TouchableOpacity <FocusableTouchableOpacity
activeOpacity={0.85} activeOpacity={0.85}
style={[styles.saveButton, { backgroundColor: currentTheme.colors.elevation2, borderColor: currentTheme.colors.elevation2 }]} style={[styles.saveButton, { backgroundColor: currentTheme.colors.elevation2, borderColor: currentTheme.colors.elevation2 }]}
onPress={handleSave} onPress={handleSave}
@ -199,9 +200,9 @@ const AccountManageScreen: React.FC = () => {
<Text style={styles.saveText}>Save changes</Text> <Text style={styles.saveText}>Save changes</Text>
</> </>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
activeOpacity={0.85} activeOpacity={0.85}
style={[ style={[
styles.signOutButton, styles.signOutButton,
@ -211,7 +212,7 @@ const AccountManageScreen: React.FC = () => {
> >
<MaterialIcons name="logout" size={18} color="#fff" style={{ marginRight: 8 }} /> <MaterialIcons name="logout" size={18} color="#fff" style={{ marginRight: 8 }} />
<Text style={styles.signOutText}>Sign out</Text> <Text style={styles.signOutText}>Sign out</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</Animated.View> </Animated.View>
<CustomAlert <CustomAlert
visible={alertVisible} visible={alertVisible}

View file

@ -5,7 +5,6 @@ import {
StyleSheet, StyleSheet,
FlatList, FlatList,
TextInput, TextInput,
TouchableOpacity,
ActivityIndicator, ActivityIndicator,
SafeAreaView, SafeAreaView,
StatusBar, StatusBar,
@ -48,6 +47,7 @@ if (Platform.OS === 'ios') {
// Removed community blur and expo-constants for Android overlay // Removed community blur and expo-constants for Android overlay
import axios from 'axios'; import axios from 'axios';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Extend Manifest type to include logo only (remove disabled status) // Extend Manifest type to include logo only (remove disabled status)
interface ExtendedManifest extends Manifest { interface ExtendedManifest extends Manifest {
@ -974,7 +974,7 @@ const AddonsScreen = () => {
<View style={styles.addonItem}> <View style={styles.addonItem}>
{reorderMode && ( {reorderMode && (
<View style={styles.reorderButtons}> <View style={styles.reorderButtons}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.reorderButton, isFirstItem && styles.disabledButton]} style={[styles.reorderButton, isFirstItem && styles.disabledButton]}
onPress={() => moveAddonUp(item)} onPress={() => moveAddonUp(item)}
disabled={isFirstItem} disabled={isFirstItem}
@ -984,8 +984,8 @@ const AddonsScreen = () => {
size={20} size={20}
color={isFirstItem ? colors.mediumGray : colors.white} color={isFirstItem ? colors.mediumGray : colors.white}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.reorderButton, isLastItem && styles.disabledButton]} style={[styles.reorderButton, isLastItem && styles.disabledButton]}
onPress={() => moveAddonDown(item)} onPress={() => moveAddonDown(item)}
disabled={isLastItem} disabled={isLastItem}
@ -995,7 +995,7 @@ const AddonsScreen = () => {
size={20} size={20}
color={isLastItem ? colors.mediumGray : colors.white} color={isLastItem ? colors.mediumGray : colors.white}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}
@ -1030,20 +1030,20 @@ const AddonsScreen = () => {
{!reorderMode ? ( {!reorderMode ? (
<> <>
{isConfigurable && ( {isConfigurable && (
<TouchableOpacity <FocusableTouchableOpacity
style={styles.configButton} style={styles.configButton}
onPress={() => handleConfigureAddon(item, item.transport)} onPress={() => handleConfigureAddon(item, item.transport)}
> >
<MaterialIcons name="settings" size={20} color={colors.primary} /> <MaterialIcons name="settings" size={20} color={colors.primary} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
{!stremioService.isPreInstalledAddon(item.id) && ( {!stremioService.isPreInstalledAddon(item.id) && (
<TouchableOpacity <FocusableTouchableOpacity
style={styles.deleteButton} style={styles.deleteButton}
onPress={() => handleRemoveAddon(item)} onPress={() => handleRemoveAddon(item)}
> >
<MaterialIcons name="delete" size={20} color={colors.error} /> <MaterialIcons name="delete" size={20} color={colors.error} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</> </>
) : ( ) : (
@ -1098,14 +1098,14 @@ const AddonsScreen = () => {
</View> </View>
<View style={styles.addonActionButtons}> <View style={styles.addonActionButtons}>
{isConfigurable && ( {isConfigurable && (
<TouchableOpacity <FocusableTouchableOpacity
style={styles.configButton} style={styles.configButton}
onPress={() => handleConfigureAddon(manifest, transportUrl)} onPress={() => handleConfigureAddon(manifest, transportUrl)}
> >
<MaterialIcons name="settings" size={20} color={colors.primary} /> <MaterialIcons name="settings" size={20} color={colors.primary} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.installButton, installing && { opacity: 0.6 }]} style={[styles.installButton, installing && { opacity: 0.6 }]}
onPress={() => handleAddAddon(transportUrl)} onPress={() => handleAddAddon(transportUrl)}
disabled={installing} disabled={installing}
@ -1115,7 +1115,7 @@ const AddonsScreen = () => {
) : ( ) : (
<MaterialIcons name="add" size={20} color={colors.white} /> <MaterialIcons name="add" size={20} color={colors.white} />
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
); );
@ -1134,17 +1134,17 @@ const AddonsScreen = () => {
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<MaterialIcons name="chevron-left" size={28} color={colors.white} /> <MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={styles.backText}>Settings</Text> <Text style={styles.backText}>Settings</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.headerActions}> <View style={styles.headerActions}>
{/* Reorder Mode Toggle Button */} {/* Reorder Mode Toggle Button */}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.headerButton, reorderMode && styles.activeHeaderButton]} style={[styles.headerButton, reorderMode && styles.activeHeaderButton]}
onPress={toggleReorderMode} onPress={toggleReorderMode}
> >
@ -1153,10 +1153,10 @@ const AddonsScreen = () => {
size={24} size={24}
color={reorderMode ? colors.primary : colors.white} color={reorderMode ? colors.primary : colors.white}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
{/* Refresh Button */} {/* Refresh Button */}
<TouchableOpacity <FocusableTouchableOpacity
style={styles.headerButton} style={styles.headerButton}
onPress={refreshAddons} onPress={refreshAddons}
disabled={loading} disabled={loading}
@ -1166,7 +1166,7 @@ const AddonsScreen = () => {
size={24} size={24}
color={loading ? colors.mediumGray : colors.white} color={loading ? colors.mediumGray : colors.white}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
@ -1221,7 +1221,7 @@ const AddonsScreen = () => {
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
/> />
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.addButton, { opacity: installing || !addonUrl ? 0.6 : 1 }]} style={[styles.addButton, { opacity: installing || !addonUrl ? 0.6 : 1 }]}
onPress={() => handleAddAddon()} onPress={() => handleAddAddon()}
disabled={installing || !addonUrl} disabled={installing || !addonUrl}
@ -1229,7 +1229,7 @@ const AddonsScreen = () => {
<Text style={styles.addButtonText}> <Text style={styles.addButtonText}>
{installing ? 'Loading...' : 'Add Addon'} {installing ? 'Loading...' : 'Add Addon'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
)} )}
@ -1289,14 +1289,14 @@ const AddonsScreen = () => {
</View> </View>
<View style={styles.addonActions}> <View style={styles.addonActions}>
{promoAddon.behaviorHints?.configurable && ( {promoAddon.behaviorHints?.configurable && (
<TouchableOpacity <FocusableTouchableOpacity
style={styles.configButton} style={styles.configButton}
onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)} onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
> >
<MaterialIcons name="settings" size={20} color={colors.primary} /> <MaterialIcons name="settings" size={20} color={colors.primary} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
<TouchableOpacity <FocusableTouchableOpacity
style={styles.installButton} style={styles.installButton}
onPress={() => handleAddAddon(PROMO_ADDON_URL)} onPress={() => handleAddAddon(PROMO_ADDON_URL)}
disabled={installing} disabled={installing}
@ -1306,7 +1306,7 @@ const AddonsScreen = () => {
) : ( ) : (
<MaterialIcons name="add" size={20} color={colors.white} /> <MaterialIcons name="add" size={20} color={colors.white} />
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
<Text style={styles.addonDescription}> <Text style={styles.addonDescription}>
@ -1371,14 +1371,14 @@ const AddonsScreen = () => {
</View> </View>
<View style={styles.addonActions}> <View style={styles.addonActions}>
{item.manifest.behaviorHints?.configurable && ( {item.manifest.behaviorHints?.configurable && (
<TouchableOpacity <FocusableTouchableOpacity
style={styles.configButton} style={styles.configButton}
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)} onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
> >
<MaterialIcons name="settings" size={20} color={colors.primary} /> <MaterialIcons name="settings" size={20} color={colors.primary} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.installButton, installing && { opacity: 0.6 }]} style={[styles.installButton, installing && { opacity: 0.6 }]}
onPress={() => handleAddAddon(item.transportUrl)} onPress={() => handleAddAddon(item.transportUrl)}
disabled={installing} disabled={installing}
@ -1388,7 +1388,7 @@ const AddonsScreen = () => {
) : ( ) : (
<MaterialIcons name="add" size={20} color={colors.white} /> <MaterialIcons name="add" size={20} color={colors.white} />
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
@ -1435,14 +1435,14 @@ const AddonsScreen = () => {
<> <>
<View style={styles.modalHeader}> <View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Install Addon</Text> <Text style={styles.modalTitle}>Install Addon</Text>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => { onPress={() => {
setShowConfirmModal(false); setShowConfirmModal(false);
setAddonDetails(null); setAddonDetails(null);
}} }}
> >
<MaterialIcons name="close" size={24} color={colors.white} /> <MaterialIcons name="close" size={24} color={colors.white} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<ScrollView <ScrollView
@ -1504,7 +1504,7 @@ const AddonsScreen = () => {
</ScrollView> </ScrollView>
<View style={styles.modalActions}> <View style={styles.modalActions}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.modalButton, styles.cancelButton]} style={[styles.modalButton, styles.cancelButton]}
onPress={() => { onPress={() => {
setShowConfirmModal(false); setShowConfirmModal(false);
@ -1512,8 +1512,8 @@ const AddonsScreen = () => {
}} }}
> >
<Text style={styles.modalButtonText}>Cancel</Text> <Text style={styles.modalButtonText}>Cancel</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.modalButton, styles.installButton]} style={[styles.modalButton, styles.installButton]}
onPress={confirmInstallAddon} onPress={confirmInstallAddon}
disabled={installing} disabled={installing}
@ -1523,7 +1523,7 @@ const AddonsScreen = () => {
) : ( ) : (
<Text style={styles.modalButtonText}>Install</Text> <Text style={styles.modalButtonText}>Install</Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</> </>
)} )}

View file

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; 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 { mmkvStorage } from '../services/mmkvStorage';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
@ -9,6 +9,7 @@ import { useNavigation, useRoute } from '@react-navigation/native';
import * as Haptics from 'expo-haptics'; import * as Haptics from 'expo-haptics';
import { useToast } from '../contexts/ToastContext'; import { useToast } from '../contexts/ToastContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
@ -277,9 +278,9 @@ const AuthScreen: React.FC = () => {
]} ]}
> >
{navigation.canGoBack() && ( {navigation.canGoBack() && (
<TouchableOpacity onPress={() => 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 }}> <FocusableTouchableOpacity onPress={() => 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 }}>
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} /> <MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
<Animated.Text style={[styles.heading, { color: currentTheme.colors.white, opacity: titleOpacity, transform: [{ translateY: titleTranslateY }] }]}> <Animated.Text style={[styles.heading, { color: currentTheme.colors.white, opacity: titleOpacity, transform: [{ translateY: titleTranslateY }] }]}>
{mode === 'signin' ? 'Welcome back' : 'Create your account'} {mode === 'signin' ? 'Welcome back' : 'Create your account'}
@ -299,7 +300,7 @@ const AuthScreen: React.FC = () => {
}, },
]} ]}
> >
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.warningCard, { backgroundColor: 'rgba(255, 193, 7, 0.1)', borderColor: 'rgba(255, 193, 7, 0.3)' }]} style={[styles.warningCard, { backgroundColor: 'rgba(255, 193, 7, 0.1)', borderColor: 'rgba(255, 193, 7, 0.3)' }]}
onPress={toggleWarningDetails} onPress={toggleWarningDetails}
activeOpacity={0.8} activeOpacity={0.8}
@ -316,7 +317,7 @@ const AuthScreen: React.FC = () => {
Read more {showWarningDetails ? '▼' : '▶'} Read more {showWarningDetails ? '▼' : '▶'}
</Text> </Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
{/* Expanded Details */} {/* Expanded Details */}
{showWarningDetails && ( {showWarningDetails && (
@ -392,7 +393,7 @@ const AuthScreen: React.FC = () => {
}, },
]} ]}
/> />
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.switchButton, styles.switchButton,
]} ]}
@ -402,8 +403,8 @@ const AuthScreen: React.FC = () => {
<Text style={[styles.switchText, { color: mode === 'signin' ? '#fff' : currentTheme.colors.textMuted }]}> <Text style={[styles.switchText, { color: mode === 'signin' ? '#fff' : currentTheme.colors.textMuted }]}>
Sign In Sign In
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.switchButton, styles.switchButton,
signupDisabled && styles.disabledButton, signupDisabled && styles.disabledButton,
@ -420,7 +421,7 @@ const AuthScreen: React.FC = () => {
]}> ]}>
Sign Up {signupDisabled && '(Disabled)'} Sign Up {signupDisabled && '(Disabled)'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
{/* Email Input */} {/* Email Input */}
@ -477,13 +478,13 @@ const AuthScreen: React.FC = () => {
returnKeyType="done" returnKeyType="done"
onSubmitEditing={handleSubmit} onSubmitEditing={handleSubmit}
/> />
<TouchableOpacity onPress={() => setShowPassword(p => !p)} style={styles.eyeButton}> <FocusableTouchableOpacity onPress={() => setShowPassword(p => !p)} style={styles.eyeButton}>
<MaterialIcons <MaterialIcons
name={showPassword ? 'visibility-off' : 'visibility'} name={showPassword ? 'visibility-off' : 'visibility'}
size={16} size={16}
color={currentTheme.colors.textMuted} color={currentTheme.colors.textMuted}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
{Platform.OS !== 'android' && isPasswordValid && ( {Platform.OS !== 'android' && isPasswordValid && (
<MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} /> <MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} />
)} )}
@ -515,13 +516,13 @@ const AuthScreen: React.FC = () => {
returnKeyType="done" returnKeyType="done"
onSubmitEditing={handleSubmit} onSubmitEditing={handleSubmit}
/> />
<TouchableOpacity onPress={() => setShowConfirm(p => !p)} style={styles.eyeButton}> <FocusableTouchableOpacity onPress={() => setShowConfirm(p => !p)} style={styles.eyeButton}>
<MaterialIcons <MaterialIcons
name={showConfirm ? 'visibility-off' : 'visibility'} name={showConfirm ? 'visibility-off' : 'visibility'}
size={16} size={16}
color={currentTheme.colors.textMuted} color={currentTheme.colors.textMuted}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
{Platform.OS !== 'android' && passwordsMatch && isConfirmValid && ( {Platform.OS !== 'android' && passwordsMatch && isConfirmValid && (
<MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} /> <MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} />
)} )}
@ -539,7 +540,7 @@ const AuthScreen: React.FC = () => {
{/* Submit Button */} {/* Submit Button */}
<Animated.View style={{ transform: [{ scale: ctaScale }] }}> <Animated.View style={{ transform: [{ scale: ctaScale }] }}>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.ctaButton, styles.ctaButton,
{ {
@ -579,12 +580,12 @@ const AuthScreen: React.FC = () => {
{mode === 'signin' ? 'Sign In' : 'Create Account'} {mode === 'signin' ? 'Sign In' : 'Create Account'}
</Animated.Text> </Animated.Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
</Animated.View> </Animated.View>
{/* Switch Mode */} {/* Switch Mode */}
{!signupDisabled && ( {!signupDisabled && (
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')} onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
activeOpacity={0.7} activeOpacity={0.7}
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
@ -595,7 +596,7 @@ const AuthScreen: React.FC = () => {
{mode === 'signin' ? 'Sign up' : 'Sign in'} {mode === 'signin' ? 'Sign up' : 'Sign in'}
</Text> </Text>
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
{/* Signup disabled message */} {/* Signup disabled message */}
@ -608,7 +609,7 @@ const AuthScreen: React.FC = () => {
)} )}
{/* Skip sign in - more prominent when coming from onboarding */} {/* Skip sign in - more prominent when coming from onboarding */}
<TouchableOpacity <FocusableTouchableOpacity
onPress={handleSkipAuth} onPress={handleSkipAuth}
activeOpacity={0.85} activeOpacity={0.85}
style={[ style={[
@ -629,7 +630,7 @@ const AuthScreen: React.FC = () => {
}}> }}>
Continue without an account Continue without an account
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</Animated.View> </Animated.View>
</Animated.View> </Animated.View>

View file

@ -4,10 +4,10 @@ import {
Text, Text,
StyleSheet, StyleSheet,
FlatList, FlatList,
TouchableOpacity,
Dimensions, Dimensions,
ActivityIndicator, ActivityIndicator,
StatusBar, StatusBar,
Platform,
} from 'react-native'; } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native'; import { useRoute, useNavigation } from '@react-navigation/native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
@ -16,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { TMDBService } from '../services/tmdbService'; import { TMDBService } from '../services/tmdbService';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const BACKDROP_WIDTH = width * 0.9; const BACKDROP_WIDTH = width * 0.9;
@ -116,12 +117,16 @@ const BackdropGalleryScreen: React.FC = () => {
const renderHeader = () => ( const renderHeader = () => (
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={999}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} /> <MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}> <Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>
{title} {title}

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
ActivityIndicator, ActivityIndicator,
Platform, Platform,
SafeAreaView, SafeAreaView,
@ -21,6 +20,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { useBackupOptions } from '../hooks/useBackupOptions'; import { useBackupOptions } from '../hooks/useBackupOptions';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Check if running on TV platform // Check if running on TV platform
const isTV = Platform.isTV; const isTV = Platform.isTV;
@ -303,13 +303,13 @@ const BackupScreen: React.FC = () => {
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.white} /> <MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.white} />
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text> <Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.headerActions}> <View style={styles.headerActions}>
{/* Empty for now, but keeping structure consistent */} {/* Empty for now, but keeping structure consistent */}
@ -345,7 +345,7 @@ const BackupScreen: React.FC = () => {
</Text> </Text>
{/* Core Data Group */} {/* Core Data Group */}
<TouchableOpacity <FocusableTouchableOpacity
style={styles.sectionHeader} style={styles.sectionHeader}
onPress={() => toggleSection('coreData')} onPress={() => toggleSection('coreData')}
activeOpacity={0.7} activeOpacity={0.7}
@ -365,7 +365,7 @@ const BackupScreen: React.FC = () => {
> >
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} /> <MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
</Animated.View> </Animated.View>
</TouchableOpacity> </FocusableTouchableOpacity>
<Animated.View <Animated.View
style={{ style={{
maxHeight: coreDataAnim.interpolate({ maxHeight: coreDataAnim.interpolate({
@ -393,7 +393,7 @@ const BackupScreen: React.FC = () => {
</Animated.View> </Animated.View>
{/* Addons & Integrations Group */} {/* Addons & Integrations Group */}
<TouchableOpacity <FocusableTouchableOpacity
style={styles.sectionHeader} style={styles.sectionHeader}
onPress={() => toggleSection('addonsIntegrations')} onPress={() => toggleSection('addonsIntegrations')}
activeOpacity={0.7} activeOpacity={0.7}
@ -413,7 +413,7 @@ const BackupScreen: React.FC = () => {
> >
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} /> <MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
</Animated.View> </Animated.View>
</TouchableOpacity> </FocusableTouchableOpacity>
<Animated.View <Animated.View
style={{ style={{
maxHeight: addonsAnim.interpolate({ maxHeight: addonsAnim.interpolate({
@ -448,7 +448,7 @@ const BackupScreen: React.FC = () => {
</Animated.View> </Animated.View>
{/* Settings & Preferences Group */} {/* Settings & Preferences Group */}
<TouchableOpacity <FocusableTouchableOpacity
style={styles.sectionHeader} style={styles.sectionHeader}
onPress={() => toggleSection('settingsPreferences')} onPress={() => toggleSection('settingsPreferences')}
activeOpacity={0.7} activeOpacity={0.7}
@ -468,7 +468,7 @@ const BackupScreen: React.FC = () => {
> >
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} /> <MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
</Animated.View> </Animated.View>
</TouchableOpacity> </FocusableTouchableOpacity>
<Animated.View <Animated.View
style={{ style={{
maxHeight: settingsAnim.interpolate({ maxHeight: settingsAnim.interpolate({
@ -516,7 +516,7 @@ const BackupScreen: React.FC = () => {
Backup & Restore Backup & Restore
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.actionButton, styles.actionButton,
{ {
@ -535,9 +535,9 @@ const BackupScreen: React.FC = () => {
<Text style={styles.actionButtonText}>Create Backup</Text> <Text style={styles.actionButtonText}>Create Backup</Text>
</> </>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.actionButton, styles.actionButton,
{ {
@ -550,7 +550,7 @@ const BackupScreen: React.FC = () => {
> >
<MaterialIcons name="restore" size={20} color="white" /> <MaterialIcons name="restore" size={20} color="white" />
<Text style={styles.actionButtonText}>Restore from Backup</Text> <Text style={styles.actionButtonText}>Restore from Backup</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
{/* Info Section */} {/* Info Section */}

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
ActivityIndicator, ActivityIndicator,
SafeAreaView, SafeAreaView,
StatusBar, StatusBar,
@ -42,6 +41,7 @@ import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
import { mmkvStorage } from '../services/mmkvStorage'; import { mmkvStorage } from '../services/mmkvStorage';
import { catalogService, DataSource, StreamingContent } from '../services/catalogService'; import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
import { tmdbService } from '../services/tmdbService'; import { tmdbService } from '../services/tmdbService';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
type CatalogScreenProps = { type CatalogScreenProps = {
route: RouteProp<RootStackParamList, 'Catalog'>; route: RouteProp<RootStackParamList, 'Catalog'>;
@ -762,7 +762,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
const aspectRatio = shape === 'landscape' ? 16 / 9 : (shape === 'square' ? 1 : 2 / 3); const aspectRatio = shape === 'landscape' ? 16 / 9 : (shape === 'square' ? 1 : 2 / 3);
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.item, styles.item,
{ {
@ -772,6 +772,9 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
]} ]}
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type, addonId })} onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type, addonId })}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="poster"
focusBorderRadius={12}
> >
<FastImage <FastImage
source={{ uri: optimizePosterUrl(item.poster) }} source={{ uri: optimizePosterUrl(item.poster) }}
@ -837,7 +840,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
{item.name} {item.name}
</Text> </Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}, [navigation, styles, effectiveNumColumns, effectiveItemWidth, screenData, type, nowPlayingMovies, colors.white, colors.mediumGray, optimizePosterUrl, addonId, isDarkMode, showTitles]); }, [navigation, styles, effectiveNumColumns, effectiveItemWidth, screenData, type, nowPlayingMovies, colors.white, colors.mediumGray, optimizePosterUrl, addonId, isDarkMode, showTitles]);
@ -847,12 +850,16 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
<Text style={styles.emptyText}> <Text style={styles.emptyText}>
No content found No content found
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.button} style={styles.button}
onPress={handleRefresh} onPress={handleRefresh}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
hasTVPreferredFocus={Platform.isTV}
> >
<Text style={styles.buttonText}>Try Again</Text> <Text style={styles.buttonText}>Try Again</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
@ -862,12 +869,16 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
<Text style={styles.errorText}> <Text style={styles.errorText}>
{error} {error}
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.button} style={styles.button}
onPress={() => loadItems(true)} onPress={() => loadItems(true)}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
hasTVPreferredFocus={Platform.isTV}
> >
<Text style={styles.buttonText}>Retry</Text> <Text style={styles.buttonText}>Retry</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
@ -885,13 +896,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="chevron-left" size={28} color={colors.white} /> <MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={styles.backText}>Back</Text> <Text style={styles.backText}>Back</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<Text style={styles.headerTitle}>{displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text> <Text style={styles.headerTitle}>{displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
{renderLoadingState()} {renderLoadingState()}
@ -904,13 +919,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="chevron-left" size={28} color={colors.white} /> <MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={styles.backText}>Back</Text> <Text style={styles.backText}>Back</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text> <Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
{renderErrorState()} {renderErrorState()}
@ -922,13 +941,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="chevron-left" size={28} color={colors.white} /> <MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={styles.backText}>Back</Text> <Text style={styles.backText}>Back</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text> <Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
@ -943,18 +966,21 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
{catalogExtras.map(extra => ( {catalogExtras.map(extra => (
<React.Fragment key={extra.name}> <React.Fragment key={extra.name}>
{/* All option - clears filter */} {/* All option - clears filter */}
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.filterChip, styles.filterChip,
(extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipActive (extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipActive
]} ]}
onPress={() => handleFilterChange(extra.name, undefined)} onPress={() => handleFilterChange(extra.name, undefined)}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={16}
> >
<Text style={[ <Text style={[
styles.filterChipText, styles.filterChipText,
(extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipTextActive (extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipTextActive
]}>All</Text> ]}>All</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
{/* Filter options from catalog extra */} {/* Filter options from catalog extra */}
{extra.options?.map(option => { {extra.options?.map(option => {
@ -962,15 +988,19 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
? activeGenreFilter === option ? activeGenreFilter === option
: selectedFilters[extra.name] === option; : selectedFilters[extra.name] === option;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={option} key={option}
style={[styles.filterChip, isActive && styles.filterChipActive]} style={[styles.filterChip, isActive && styles.filterChipActive]}
onPress={() => handleFilterChange(extra.name, option)} onPress={() => handleFilterChange(extra.name, option)}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV && isActive}
> >
<Text style={[styles.filterChipText, isActive && styles.filterChipTextActive]}> <Text style={[styles.filterChipText, isActive && styles.filterChipTextActive]}>
{option} {option}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
})} })}
</React.Fragment> </React.Fragment>

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
ScrollView, ScrollView,
StatusBar, StatusBar,
Platform, Platform,
@ -17,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// TTL options in milliseconds - organized in rows // TTL options in milliseconds - organized in rows
const TTL_OPTIONS = [ const TTL_OPTIONS = [
@ -132,7 +132,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => { const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => {
const isSelected = settings.streamCacheTTL === option.value; const isSelected = settings.streamCacheTTL === option.value;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.ttlOption, styles.ttlOption,
{ {
@ -142,6 +142,10 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
]} ]}
onPress={() => handleUpdateSetting('streamCacheTTL', option.value)} onPress={() => handleUpdateSetting('streamCacheTTL', option.value)}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={8}
hasTVPreferredFocus={Platform.isTV && isSelected}
> >
<Text style={[ <Text style={[
styles.ttlOptionText, styles.ttlOptionText,
@ -152,7 +156,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
{isSelected && ( {isSelected && (
<MaterialIcons name="check" size={20} color={colors.white} /> <MaterialIcons name="check" size={20} color={colors.white} />
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -162,13 +166,17 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={handleBack} onPress={handleBack}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="chevron-left" size={28} color={colors.white} /> <MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={styles.backText}>Settings</Text> <Text style={styles.backText}>Settings</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<Text style={styles.headerTitle}> <Text style={styles.headerTitle}>

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
ScrollView, ScrollView,
SafeAreaView, SafeAreaView,
StatusBar, StatusBar,
@ -24,6 +23,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { fetchContributors, GitHubContributor } from '../services/githubReleaseService'; import { fetchContributors, GitHubContributor } from '../services/githubReleaseService';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
const isTablet = width >= 768; const isTablet = width >= 768;
@ -91,7 +91,7 @@ const ContributorCard: React.FC<ContributorCardProps> = ({ contributor, currentT
}, [contributor.html_url]); }, [contributor.html_url]);
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.contributorCard, styles.contributorCard,
{ backgroundColor: currentTheme.colors.elevation1 }, { backgroundColor: currentTheme.colors.elevation1 },
@ -130,7 +130,7 @@ const ContributorCard: React.FC<ContributorCardProps> = ({ contributor, currentT
color={currentTheme.colors.mediumEmphasis} color={currentTheme.colors.mediumEmphasis}
style={styles.externalIcon} style={styles.externalIcon}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -164,7 +164,7 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
const defaultAvatar = `https://cdn.discordapp.com/embed/avatars/0.png`; const defaultAvatar = `https://cdn.discordapp.com/embed/avatars/0.png`;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.contributorCard, styles.contributorCard,
{ backgroundColor: currentTheme.colors.elevation1 }, { backgroundColor: currentTheme.colors.elevation1 },
@ -230,7 +230,7 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
color={currentTheme.colors.mediumEmphasis} color={currentTheme.colors.mediumEmphasis}
style={styles.externalIcon} style={styles.externalIcon}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -422,13 +422,13 @@ const ContributorsScreen: React.FC = () => {
<StatusBar barStyle={'light-content'} /> <StatusBar barStyle={'light-content'} />
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}> <View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} /> <Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text> <Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<Text style={[ <Text style={[
styles.headerTitle, styles.headerTitle,
@ -457,13 +457,13 @@ const ContributorsScreen: React.FC = () => {
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}> <View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} /> <Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text> <Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<Text style={[ <Text style={[
styles.headerTitle, styles.headerTitle,
@ -480,7 +480,7 @@ const ContributorsScreen: React.FC = () => {
{ backgroundColor: currentTheme.colors.elevation1 }, { backgroundColor: currentTheme.colors.elevation1 },
isTablet && styles.tabletTabSwitcher isTablet && styles.tabletTabSwitcher
]}> ]}>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.tab, styles.tab,
activeTab === 'contributors' && { backgroundColor: currentTheme.colors.primary }, activeTab === 'contributors' && { backgroundColor: currentTheme.colors.primary },
@ -496,8 +496,8 @@ const ContributorsScreen: React.FC = () => {
]}> ]}>
Contributors Contributors
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.tab, styles.tab,
activeTab === 'special' && { backgroundColor: currentTheme.colors.primary }, activeTab === 'special' && { backgroundColor: currentTheme.colors.primary },
@ -513,7 +513,7 @@ const ContributorsScreen: React.FC = () => {
]}> ]}>
Special Mentions Special Mentions
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<View style={styles.content}> <View style={styles.content}>
@ -530,14 +530,14 @@ const ContributorsScreen: React.FC = () => {
<Text style={[styles.errorSubtext, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.errorSubtext, { color: currentTheme.colors.mediumEmphasis }]}>
GitHub API rate limit exceeded. Please try again later or pull to refresh. GitHub API rate limit exceeded. Please try again later or pull to refresh.
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => loadContributors()} onPress={() => loadContributors()}
> >
<Text style={[styles.retryText, { color: currentTheme.colors.white }]}> <Text style={[styles.retryText, { color: currentTheme.colors.white }]}>
Try Again Try Again
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
) : contributors.length === 0 ? ( ) : contributors.length === 0 ? (
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>

View file

@ -4,7 +4,6 @@ import {
Text, Text,
StyleSheet, StyleSheet,
TextInput, TextInput,
TouchableOpacity,
SafeAreaView, SafeAreaView,
StatusBar, StatusBar,
Platform, Platform,
@ -27,6 +26,7 @@ import { logger } from '../utils/logger';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { mmkvStorage } from '../services/mmkvStorage'; import { mmkvStorage } from '../services/mmkvStorage';
import axios from 'axios'; import axios from 'axios';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const TORBOX_STORAGE_KEY = 'torbox_debrid_config'; const TORBOX_STORAGE_KEY = 'torbox_debrid_config';
@ -1132,15 +1132,18 @@ const DebridIntegrationScreen = () => {
</View> </View>
</View> </View>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.actionButton, styles.dangerButton, loading && styles.disabledButton]} style={[styles.actionButton, styles.dangerButton, loading && styles.disabledButton]}
onPress={handleDisconnect} onPress={handleDisconnect}
disabled={loading} disabled={loading}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<Text style={styles.buttonText}> <Text style={styles.buttonText}>
{loading ? 'Disconnecting...' : 'Disconnect & Remove'} {loading ? 'Disconnecting...' : 'Disconnect & Remove'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
{userData && ( {userData && (
<View style={styles.userDataCard}> <View style={styles.userDataCard}>
@ -1213,12 +1216,15 @@ const DebridIntegrationScreen = () => {
<Text style={styles.sectionText}> <Text style={styles.sectionText}>
Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings. Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.subscribeButton} style={styles.subscribeButton}
onPress={() => Linking.openURL('https://torbox.app/settings?section=integration-settings')} onPress={() => Linking.openURL('https://torbox.app/settings?section=integration-settings')}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<Text style={styles.subscribeButtonText}>Open Settings</Text> <Text style={styles.subscribeButtonText}>Open Settings</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</> </>
) : ( ) : (
@ -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. Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.
</Text> </Text>
<TouchableOpacity onPress={() => Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')} style={styles.guideLink}> <FocusableTouchableOpacity
onPress={() => Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')}
style={styles.guideLink}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={12}
>
<Text style={styles.guideLinkText}>What is a Debrid Service?</Text> <Text style={styles.guideLinkText}>What is a Debrid Service?</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.inputContainer}> <View style={styles.inputContainer}>
<Text style={styles.label}>Torbox API Key</Text> <Text style={styles.label}>Torbox API Key</Text>
@ -1245,24 +1257,33 @@ const DebridIntegrationScreen = () => {
/> />
</View> </View>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.connectButton, loading && styles.disabledButton]} style={[styles.connectButton, loading && styles.disabledButton]}
onPress={handleConnect} onPress={handleConnect}
disabled={loading} disabled={loading}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<Text style={styles.connectButtonText}> <Text style={styles.connectButtonText}>
{loading ? 'Connecting...' : 'Connect & Install'} {loading ? 'Connecting...' : 'Connect & Install'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Unlock Premium Speeds</Text> <Text style={styles.sectionTitle}>Unlock Premium Speeds</Text>
<Text style={styles.sectionText}> <Text style={styles.sectionText}>
Get a Torbox subscription to access cached high-quality streams with zero buffering. Get a Torbox subscription to access cached high-quality streams with zero buffering.
</Text> </Text>
<TouchableOpacity style={styles.subscribeButton} onPress={openSubscription}> <FocusableTouchableOpacity
style={styles.subscribeButton}
onPress={openSubscription}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
>
<Text style={styles.subscribeButtonText}>Get Subscription</Text> <Text style={styles.subscribeButtonText}>Get Subscription</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</> </>
)} )}
@ -1306,12 +1327,15 @@ const DebridIntegrationScreen = () => {
<Text style={styles.promoText}> <Text style={styles.promoText}>
Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads. Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.promoButton} style={styles.promoButton}
onPress={() => Linking.openURL('https://torbox.app/subscription?referral=493192f2-6403-440f-b414-768f72222ec7')} onPress={() => Linking.openURL('https://torbox.app/subscription?referral=493192f2-6403-440f-b414-768f72222ec7')}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<Text style={styles.promoButtonText}>Get TorBox Subscription</Text> <Text style={styles.promoButtonText}>Get TorBox Subscription</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}
@ -1320,13 +1344,17 @@ const DebridIntegrationScreen = () => {
<Text style={styles.configSectionTitle}>Debrid Service *</Text> <Text style={styles.configSectionTitle}>Debrid Service *</Text>
<View style={styles.pickerContainer}> <View style={styles.pickerContainer}>
{TORRENTIO_DEBRID_SERVICES.map((service: any) => ( {TORRENTIO_DEBRID_SERVICES.map((service: any) => (
<TouchableOpacity <FocusableTouchableOpacity
key={service.id} key={service.id}
style={[ style={[
styles.pickerItem, styles.pickerItem,
torrentioConfig.debridService === service.id && styles.pickerItemSelected torrentioConfig.debridService === service.id && styles.pickerItemSelected
]} ]}
onPress={() => setTorrentioConfig(prev => ({ ...prev, debridService: service.id }))} onPress={() => setTorrentioConfig(prev => ({ ...prev, debridService: service.id }))}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && torrentioConfig.debridService === service.id}
> >
<Text style={[ <Text style={[
styles.pickerItemText, styles.pickerItemText,
@ -1334,7 +1362,7 @@ const DebridIntegrationScreen = () => {
]}> ]}>
{service.name} {service.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
</View> </View>
@ -1355,9 +1383,12 @@ const DebridIntegrationScreen = () => {
</View> </View>
{/* Sorting - Accordion */} {/* Sorting - Accordion */}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.accordionHeader, expandedSections.sorting && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]} style={[styles.accordionHeader, expandedSections.sorting && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
onPress={() => toggleSection('sorting')} onPress={() => toggleSection('sorting')}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={16}
> >
<View> <View>
<Text style={styles.accordionHeaderText}>Sorting</Text> <Text style={styles.accordionHeaderText}>Sorting</Text>
@ -1366,29 +1397,36 @@ const DebridIntegrationScreen = () => {
</Text> </Text>
</View> </View>
<Feather name={expandedSections.sorting ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} /> <Feather name={expandedSections.sorting ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
{expandedSections.sorting && ( {expandedSections.sorting && (
<View style={styles.accordionContent}> <View style={styles.accordionContent}>
<View style={styles.pickerContainer}> <View style={styles.pickerContainer}>
{TORRENTIO_SORT_OPTIONS.map(option => ( {TORRENTIO_SORT_OPTIONS.map(option => (
<TouchableOpacity <FocusableTouchableOpacity
key={option.id} key={option.id}
style={[styles.pickerItem, torrentioConfig.sort === option.id && styles.pickerItemSelected]} style={[styles.pickerItem, torrentioConfig.sort === option.id && styles.pickerItemSelected]}
onPress={() => setTorrentioConfig(prev => ({ ...prev, sort: option.id }))} onPress={() => setTorrentioConfig(prev => ({ ...prev, sort: option.id }))}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && torrentioConfig.sort === option.id}
> >
<Text style={[styles.pickerItemText, torrentioConfig.sort === option.id && styles.pickerItemTextSelected]}> <Text style={[styles.pickerItemText, torrentioConfig.sort === option.id && styles.pickerItemTextSelected]}>
{option.name} {option.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
</View> </View>
)} )}
{/* Quality Filter - Accordion */} {/* Quality Filter - Accordion */}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.accordionHeader, expandedSections.qualityFilter && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]} style={[styles.accordionHeader, expandedSections.qualityFilter && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
onPress={() => toggleSection('qualityFilter')} onPress={() => toggleSection('qualityFilter')}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={16}
> >
<View> <View>
<Text style={styles.accordionHeaderText}>Exclude Qualities</Text> <Text style={styles.accordionHeaderText}>Exclude Qualities</Text>
@ -1397,29 +1435,36 @@ const DebridIntegrationScreen = () => {
</Text> </Text>
</View> </View>
<Feather name={expandedSections.qualityFilter ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} /> <Feather name={expandedSections.qualityFilter ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
{expandedSections.qualityFilter && ( {expandedSections.qualityFilter && (
<View style={styles.accordionContent}> <View style={styles.accordionContent}>
<View style={styles.chipContainer}> <View style={styles.chipContainer}>
{TORRENTIO_QUALITY_FILTERS.map(quality => ( {TORRENTIO_QUALITY_FILTERS.map(quality => (
<TouchableOpacity <FocusableTouchableOpacity
key={quality.id} key={quality.id}
style={[styles.chip, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipSelected]} style={[styles.chip, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipSelected]}
onPress={() => toggleQualityFilter(quality.id)} onPress={() => toggleQualityFilter(quality.id)}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={999}
hasTVPreferredFocus={Platform.isTV && torrentioConfig.qualityFilter.includes(quality.id)}
> >
<Text style={[styles.chipText, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipTextSelected]}> <Text style={[styles.chipText, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipTextSelected]}>
{quality.name} {quality.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
</View> </View>
)} )}
{/* Priority Languages - Accordion */} {/* Priority Languages - Accordion */}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.accordionHeader, expandedSections.languages && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]} style={[styles.accordionHeader, expandedSections.languages && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
onPress={() => toggleSection('languages')} onPress={() => toggleSection('languages')}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={16}
> >
<View> <View>
<Text style={styles.accordionHeaderText}>Priority Languages</Text> <Text style={styles.accordionHeaderText}>Priority Languages</Text>
@ -1428,29 +1473,36 @@ const DebridIntegrationScreen = () => {
</Text> </Text>
</View> </View>
<Feather name={expandedSections.languages ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} /> <Feather name={expandedSections.languages ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
{expandedSections.languages && ( {expandedSections.languages && (
<View style={styles.accordionContent}> <View style={styles.accordionContent}>
<View style={styles.chipContainer}> <View style={styles.chipContainer}>
{TORRENTIO_LANGUAGES.map(lang => ( {TORRENTIO_LANGUAGES.map(lang => (
<TouchableOpacity <FocusableTouchableOpacity
key={lang.id} key={lang.id}
style={[styles.chip, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipSelected]} style={[styles.chip, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipSelected]}
onPress={() => toggleLanguage(lang.id)} onPress={() => toggleLanguage(lang.id)}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={999}
hasTVPreferredFocus={Platform.isTV && torrentioConfig.priorityLanguages.includes(lang.id)}
> >
<Text style={[styles.chipText, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipTextSelected]}> <Text style={[styles.chipText, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipTextSelected]}>
{lang.name} {lang.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
</View> </View>
)} )}
{/* Max Results - Accordion */} {/* Max Results - Accordion */}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.accordionHeader, expandedSections.maxResults && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]} style={[styles.accordionHeader, expandedSections.maxResults && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
onPress={() => toggleSection('maxResults')} onPress={() => toggleSection('maxResults')}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={16}
> >
<View> <View>
<Text style={styles.accordionHeaderText}>Max Results</Text> <Text style={styles.accordionHeaderText}>Max Results</Text>
@ -1459,36 +1511,43 @@ const DebridIntegrationScreen = () => {
</Text> </Text>
</View> </View>
<Feather name={expandedSections.maxResults ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} /> <Feather name={expandedSections.maxResults ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
{expandedSections.maxResults && ( {expandedSections.maxResults && (
<View style={styles.accordionContent}> <View style={styles.accordionContent}>
<View style={styles.pickerContainer}> <View style={styles.pickerContainer}>
{TORRENTIO_MAX_RESULTS.map(option => ( {TORRENTIO_MAX_RESULTS.map(option => (
<TouchableOpacity <FocusableTouchableOpacity
key={option.id || 'all'} key={option.id || 'all'}
style={[styles.pickerItem, torrentioConfig.maxResults === option.id && styles.pickerItemSelected]} style={[styles.pickerItem, torrentioConfig.maxResults === option.id && styles.pickerItemSelected]}
onPress={() => setTorrentioConfig(prev => ({ ...prev, maxResults: option.id }))} onPress={() => setTorrentioConfig(prev => ({ ...prev, maxResults: option.id }))}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && torrentioConfig.maxResults === option.id}
> >
<Text style={[styles.pickerItemText, torrentioConfig.maxResults === option.id && styles.pickerItemTextSelected]}> <Text style={[styles.pickerItemText, torrentioConfig.maxResults === option.id && styles.pickerItemTextSelected]}>
{option.name} {option.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
</View> </View>
)} )}
{/* Additional Options - Accordion */} {/* Additional Options - Accordion */}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.accordionHeader, expandedSections.options && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]} style={[styles.accordionHeader, expandedSections.options && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
onPress={() => toggleSection('options')} onPress={() => toggleSection('options')}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={16}
> >
<View> <View>
<Text style={styles.accordionHeaderText}>Additional Options</Text> <Text style={styles.accordionHeaderText}>Additional Options</Text>
<Text style={styles.accordionSubtext}>Catalog & download settings</Text> <Text style={styles.accordionSubtext}>Catalog & download settings</Text>
</View> </View>
<Feather name={expandedSections.options ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} /> <Feather name={expandedSections.options ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
{expandedSections.options && ( {expandedSections.options && (
<View style={styles.accordionContent}> <View style={styles.accordionContent}>
<View style={styles.switchRow}> <View style={styles.switchRow}>
@ -1526,33 +1585,42 @@ const DebridIntegrationScreen = () => {
<View style={{ marginTop: 8 }}> <View style={{ marginTop: 8 }}>
{torrentioConfig.isInstalled ? ( {torrentioConfig.isInstalled ? (
<> <>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.connectButton, torrentioLoading && styles.disabledButton]} style={[styles.connectButton, torrentioLoading && styles.disabledButton]}
onPress={handleInstallTorrentio} onPress={handleInstallTorrentio}
disabled={torrentioLoading} disabled={torrentioLoading}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<Text style={styles.connectButtonText}> <Text style={styles.connectButtonText}>
{torrentioLoading ? 'Updating...' : 'Update Configuration'} {torrentioLoading ? 'Updating...' : 'Update Configuration'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.actionButton, styles.dangerButton, torrentioLoading && styles.disabledButton]} style={[styles.actionButton, styles.dangerButton, torrentioLoading && styles.disabledButton]}
onPress={handleRemoveTorrentio} onPress={handleRemoveTorrentio}
disabled={torrentioLoading} disabled={torrentioLoading}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<Text style={styles.buttonText}>Remove Torrentio</Text> <Text style={styles.buttonText}>Remove Torrentio</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</> </>
) : ( ) : (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.connectButton, torrentioLoading && styles.disabledButton]} style={[styles.connectButton, torrentioLoading && styles.disabledButton]}
onPress={handleInstallTorrentio} onPress={handleInstallTorrentio}
disabled={torrentioLoading} disabled={torrentioLoading}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<Text style={styles.connectButtonText}> <Text style={styles.connectButtonText}>
{torrentioLoading ? 'Installing...' : 'Install Torrentio'} {torrentioLoading ? 'Installing...' : 'Install Torrentio'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
@ -1578,33 +1646,45 @@ const DebridIntegrationScreen = () => {
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} /> <StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
style={styles.backButton} style={styles.backButton}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={999}
hasTVPreferredFocus={Platform.isTV}
> >
<Feather name="arrow-left" size={24} color={colors.white} /> <Feather name="arrow-left" size={24} color={colors.white} />
</TouchableOpacity> </FocusableTouchableOpacity>
<Text style={styles.headerTitle}>Debrid Integration</Text> <Text style={styles.headerTitle}>Debrid Integration</Text>
</View> </View>
{/* Tab Selector */} {/* Tab Selector */}
<View style={styles.tabContainer}> <View style={styles.tabContainer}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.tab, activeTab === 'torbox' && styles.activeTab]} style={[styles.tab, activeTab === 'torbox' && styles.activeTab]}
onPress={() => setActiveTab('torbox')} onPress={() => setActiveTab('torbox')}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV && activeTab === 'torbox'}
> >
<Text style={[styles.tabText, activeTab === 'torbox' && styles.activeTabText]}> <Text style={[styles.tabText, activeTab === 'torbox' && styles.activeTabText]}>
TorBox TorBox
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.tab, activeTab === 'torrentio' && styles.activeTab]} style={[styles.tab, activeTab === 'torrentio' && styles.activeTab]}
onPress={() => setActiveTab('torrentio')} onPress={() => setActiveTab('torrentio')}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV && activeTab === 'torrentio'}
> >
<Text style={[styles.tabText, activeTab === 'torrentio' && styles.activeTabText]}> <Text style={[styles.tabText, activeTab === 'torrentio' && styles.activeTabText]}>
Torrentio Torrentio
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<KeyboardAvoidingView <KeyboardAvoidingView

View file

@ -5,7 +5,6 @@ import {
StyleSheet, StyleSheet,
StatusBar, StatusBar,
Dimensions, Dimensions,
TouchableOpacity,
FlatList, FlatList,
RefreshControl, RefreshControl,
Alert, Alert,
@ -35,6 +34,7 @@ import type { DownloadItem } from '../contexts/DownloadsContext';
import { useToast } from '../contexts/ToastContext'; import { useToast } from '../contexts/ToastContext';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import ScreenHeader from '../components/common/ScreenHeader'; import ScreenHeader from '../components/common/ScreenHeader';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { height, width } = Dimensions.get('window'); const { height, width } = Dimensions.get('window');
const isTablet = width >= 768; const isTablet = width >= 768;
@ -80,7 +80,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
Downloaded content will appear here for offline viewing Downloaded content will appear here for offline viewing
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => { onPress={() => {
navigation.navigate('Search'); navigation.navigate('Search');
@ -89,7 +89,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.background }]}> <Text style={[styles.exploreButtonText, { color: currentTheme.colors.background }]}>
Explore Content Explore Content
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
}; };
@ -204,7 +204,7 @@ const DownloadItemComponent: React.FC<{
}; };
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.downloadItem, { backgroundColor: currentTheme.colors.elevation2 }]} style={[styles.downloadItem, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={() => onPress(item)} onPress={() => onPress(item)}
onLongPress={handleLongPress} onLongPress={handleLongPress}
@ -314,7 +314,7 @@ const DownloadItemComponent: React.FC<{
{/* Action buttons */} {/* Action buttons */}
<View style={styles.actionContainer}> <View style={styles.actionContainer}>
{getActionIcon() && ( {getActionIcon() && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]} style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={handleActionPress} onPress={handleActionPress}
activeOpacity={0.7} activeOpacity={0.7}
@ -324,10 +324,10 @@ const DownloadItemComponent: React.FC<{
size={20} size={20}
color={currentTheme.colors.primary} color={currentTheme.colors.primary}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]} style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={() => onRequestRemove(item)} onPress={() => onRequestRemove(item)}
activeOpacity={0.7} activeOpacity={0.7}
@ -337,9 +337,9 @@ const DownloadItemComponent: React.FC<{
size={20} size={20}
color={currentTheme.colors.error} color={currentTheme.colors.error}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}); });
@ -568,7 +568,7 @@ const DownloadsScreen: React.FC = () => {
); );
const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => ( const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => (
<TouchableOpacity <FocusableTouchableOpacity
key={filter} key={filter}
style={[ style={[
styles.filterButton, styles.filterButton,
@ -612,7 +612,7 @@ const DownloadsScreen: React.FC = () => {
</Text> </Text>
</View> </View>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
); );
return ( return (
@ -627,7 +627,7 @@ const DownloadsScreen: React.FC = () => {
<ScreenHeader <ScreenHeader
title="Downloads" title="Downloads"
rightActionComponent={ rightActionComponent={
<TouchableOpacity <FocusableTouchableOpacity
style={styles.helpButton} style={styles.helpButton}
onPress={showDownloadHelp} onPress={showDownloadHelp}
activeOpacity={0.7} activeOpacity={0.7}
@ -637,7 +637,7 @@ const DownloadsScreen: React.FC = () => {
size={24} size={24}
color={currentTheme.colors.mediumEmphasis} color={currentTheme.colors.mediumEmphasis}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
} }
isTablet={isTablet} isTablet={isTablet}
> >

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
ActivityIndicator, ActivityIndicator,
SafeAreaView, SafeAreaView,
StatusBar, StatusBar,
@ -77,6 +76,7 @@ import { useToast } from '../contexts/ToastContext';
import FirstTimeWelcome from '../components/FirstTimeWelcome'; import FirstTimeWelcome from '../components/FirstTimeWelcome';
import { HeaderVisibility } from '../contexts/HeaderVisibility'; import { HeaderVisibility } from '../contexts/HeaderVisibility';
import { useTrailer } from '../contexts/TrailerContext'; import { useTrailer } from '../contexts/TrailerContext';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Constants // Constants
const CATALOG_SETTINGS_KEY = 'catalog_settings'; const CATALOG_SETTINGS_KEY = 'catalog_settings';
@ -799,15 +799,18 @@ const HomeScreen = () => {
return ( return (
<View> <View>
<View style={styles.loadMoreContainer}> <View style={styles.loadMoreContainer}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.loadMoreButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.loadMoreButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={handleLoadMoreCatalogs} onPress={handleLoadMoreCatalogs}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={16}
> >
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} /> <MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
<Text style={[styles.loadMoreText, { color: currentTheme.colors.white }]}> <Text style={[styles.loadMoreText, { color: currentTheme.colors.white }]}>
Load More Catalogs Load More Catalogs
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
); );
@ -829,13 +832,17 @@ const HomeScreen = () => {
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}> <Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
No content available No content available
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Settings')} onPress={() => navigation.navigate('Settings')}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} /> <MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text> <Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}
</> </>

View file

@ -9,7 +9,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
useColorScheme, useColorScheme,
useWindowDimensions, useWindowDimensions,
SafeAreaView, SafeAreaView,
@ -38,6 +37,7 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg';
import { traktService, TraktService, TraktImages } from '../services/traktService'; import { traktService, TraktService, TraktImages } from '../services/traktService';
import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner'; import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
interface LibraryItem extends StreamingContent { interface LibraryItem extends StreamingContent {
progress?: number; progress?: number;
@ -121,10 +121,13 @@ const TraktItem = React.memo(({
}, [navigation, item.imdbId, item.type]); }, [navigation, item.imdbId, item.type]);
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.itemContainer, { width }]} style={[styles.itemContainer, { width }]}
onPress={handlePress} onPress={handlePress}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="poster"
focusBorderRadius={12}
> >
<View> <View>
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}> <View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
@ -146,7 +149,7 @@ const TraktItem = React.memo(({
</Text> </Text>
)} )}
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}); });
@ -386,7 +389,7 @@ const LibraryScreen = () => {
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]); }, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
const renderItem = ({ item }: { item: LibraryItem }) => ( const renderItem = ({ item }: { item: LibraryItem }) => (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.itemContainer, { width: itemWidth }]} style={[styles.itemContainer, { width: itemWidth }]}
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })} onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
onLongPress={() => { onLongPress={() => {
@ -394,6 +397,9 @@ const LibraryScreen = () => {
setMenuVisible(true); setMenuVisible(true);
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="poster"
focusBorderRadius={12}
> >
<View> <View>
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}> <View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
@ -424,17 +430,20 @@ const LibraryScreen = () => {
</Text> </Text>
)} )}
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => ( const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.itemContainer, { width: itemWidth }]} style={[styles.itemContainer, { width: itemWidth }]}
onPress={() => { onPress={() => {
setSelectedTraktFolder(folder.id); setSelectedTraktFolder(folder.id);
loadAllCollections(); loadAllCollections();
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="card"
focusBorderRadius={12}
> >
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}> <View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
<View style={styles.folderGradient}> <View style={styles.folderGradient}>
@ -452,11 +461,11 @@ const LibraryScreen = () => {
</Text> </Text>
</View> </View>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
const renderTraktFolder = () => ( const renderTraktFolder = () => (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.itemContainer, { width: itemWidth }]} style={[styles.itemContainer, { width: itemWidth }]}
onPress={() => { onPress={() => {
if (!traktAuthenticated) { if (!traktAuthenticated) {
@ -468,6 +477,9 @@ const LibraryScreen = () => {
} }
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="card"
focusBorderRadius={12}
> >
<View> <View>
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}> <View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
@ -489,7 +501,7 @@ const LibraryScreen = () => {
</Text> </Text>
)} )}
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => { const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => {
@ -715,7 +727,7 @@ const LibraryScreen = () => {
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}> <Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
Your Trakt collections will appear here once you start using Trakt Your Trakt collections will appear here once you start using Trakt
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.exploreButton, { style={[styles.exploreButton, {
backgroundColor: currentTheme.colors.primary, backgroundColor: currentTheme.colors.primary,
shadowColor: currentTheme.colors.black shadowColor: currentTheme.colors.black
@ -724,9 +736,13 @@ const LibraryScreen = () => {
loadAllCollections(); loadAllCollections();
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV}
> >
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Load Collections</Text> <Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Load Collections</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
} }
@ -756,7 +772,7 @@ const LibraryScreen = () => {
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}> <Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
This collection is empty This collection is empty
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.exploreButton, { style={[styles.exploreButton, {
backgroundColor: currentTheme.colors.primary, backgroundColor: currentTheme.colors.primary,
shadowColor: currentTheme.colors.black shadowColor: currentTheme.colors.black
@ -765,9 +781,13 @@ const LibraryScreen = () => {
loadAllCollections(); loadAllCollections();
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV}
> >
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Refresh</Text> <Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Refresh</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
} }
@ -791,7 +811,7 @@ const LibraryScreen = () => {
const isActive = filter === filterType; const isActive = filter === filterType;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.filterButton, styles.filterButton,
isActive && { backgroundColor: currentTheme.colors.primary }, isActive && { backgroundColor: currentTheme.colors.primary },
@ -811,6 +831,10 @@ const LibraryScreen = () => {
setFilter(filterType); setFilter(filterType);
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={18}
hasTVPreferredFocus={Platform.isTV && isActive}
> >
{filterType === 'trakt' ? ( {filterType === 'trakt' ? (
<View style={[styles.filterIcon, { justifyContent: 'center', alignItems: 'center' }]}> <View style={[styles.filterIcon, { justifyContent: 'center', alignItems: 'center' }]}>
@ -833,7 +857,7 @@ const LibraryScreen = () => {
> >
{label} {label}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -858,16 +882,20 @@ const LibraryScreen = () => {
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}> <Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
{emptySubtitle} {emptySubtitle}
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.exploreButton, { style={[styles.exploreButton, {
backgroundColor: currentTheme.colors.primary, backgroundColor: currentTheme.colors.primary,
shadowColor: currentTheme.colors.black shadowColor: currentTheme.colors.black
}]} }]}
onPress={() => navigation.navigate('Search')} onPress={() => navigation.navigate('Search')}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV}
> >
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Find something to watch</Text> <Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Find something to watch</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
} }

View file

@ -6,7 +6,6 @@ import {
StatusBar, StatusBar,
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
TouchableOpacity,
InteractionManager, InteractionManager,
BackHandler, BackHandler,
Platform, Platform,
@ -64,6 +63,7 @@ import { useWatchProgress } from '../hooks/useWatchProgress';
import { TraktService, TraktPlaybackItem } from '../services/traktService'; import { TraktService, TraktPlaybackItem } from '../services/traktService';
import { tmdbService } from '../services/tmdbService'; import { tmdbService } from '../services/tmdbService';
import { catalogService } from '../services/catalogService'; import { catalogService } from '../services/catalogService';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { height } = Dimensions.get('window'); const { height } = Dimensions.get('window');
@ -865,21 +865,28 @@ const MetadataScreen: React.FC = () => {
{metadataError} {metadataError}
</Text> </Text>
)} )}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={loadMetadata} onPress={loadMetadata}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="refresh" size={20} color={currentTheme.colors.white} style={{ marginRight: 8 }} /> <MaterialIcons name="refresh" size={20} color={currentTheme.colors.white} style={{ marginRight: 8 }} />
<Text style={styles.retryButtonText}>Try Again</Text> <Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.backButton, { borderColor: currentTheme.colors.primary }]} style={[styles.backButton, { borderColor: currentTheme.colors.primary }]}
onPress={handleBack} onPress={handleBack}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text> <Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</SafeAreaView> </SafeAreaView>
); );
@ -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 */} {/* 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 && ( {shouldLoadSecondaryData && Object.keys(groupedEpisodes).length === 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
<View style={styles.backdropGalleryContainer}> <View style={styles.backdropGalleryContainer}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backdropGalleryButton} style={styles.backdropGalleryButton}
onPress={() => navigation.navigate('BackdropGallery' as any, { onPress={() => navigation.navigate('BackdropGallery' as any, {
tmdbId: metadata.tmdbId, tmdbId: metadata.tmdbId,
@ -1248,10 +1255,13 @@ const MetadataScreen: React.FC = () => {
title: metadata.name || 'Gallery' title: metadata.name || 'Gallery'
})} })}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={16}
> >
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text> <Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} /> <MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}
@ -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 */} {/* 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 && ( {shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
<View style={styles.backdropGalleryContainer}> <View style={styles.backdropGalleryContainer}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backdropGalleryButton} style={styles.backdropGalleryButton}
onPress={() => navigation.navigate('BackdropGallery' as any, { onPress={() => navigation.navigate('BackdropGallery' as any, {
tmdbId: metadata.tmdbId, tmdbId: metadata.tmdbId,
@ -1389,10 +1399,13 @@ const MetadataScreen: React.FC = () => {
title: metadata.name || 'Gallery' title: metadata.name || 'Gallery'
})} })}
focusable={Platform.isTV} focusable={Platform.isTV}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={16}
> >
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text> <Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} /> <MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}

View file

@ -4,7 +4,6 @@ import {
Text, Text,
StyleSheet, StyleSheet,
Dimensions, Dimensions,
TouchableOpacity,
StatusBar, StatusBar,
Platform, Platform,
} from 'react-native'; } from 'react-native';
@ -25,6 +24,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { NavigationProp, useNavigation } from '@react-navigation/native'; import { NavigationProp, useNavigation } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { mmkvStorage } from '../services/mmkvStorage'; import { mmkvStorage } from '../services/mmkvStorage';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
@ -281,9 +281,9 @@ const OnboardingScreen = () => {
entering={FadeIn.delay(300).duration(600)} entering={FadeIn.delay(300).duration(600)}
style={styles.header} style={styles.header}
> >
<TouchableOpacity onPress={handleSkip} style={styles.skipButton}> <FocusableTouchableOpacity onPress={handleSkip} style={styles.skipButton}>
<Text style={styles.skipText}>Skip</Text> <Text style={styles.skipText}>Skip</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
{/* Smooth Progress Bar */} {/* Smooth Progress Bar */}
<View style={styles.progressContainer}> <View style={styles.progressContainer}>
@ -322,7 +322,7 @@ const OnboardingScreen = () => {
</View> </View>
{/* Animated Button */} {/* Animated Button */}
<TouchableOpacity <FocusableTouchableOpacity
onPress={handleNext} onPress={handleNext}
onPressIn={handlePressIn} onPressIn={handlePressIn}
onPressOut={handlePressOut} onPressOut={handlePressOut}
@ -333,7 +333,7 @@ const OnboardingScreen = () => {
{currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Continue'} {currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Continue'}
</Text> </Text>
</Animated.View> </Animated.View>
</TouchableOpacity> </FocusableTouchableOpacity>
</Animated.View> </Animated.View>
</View> </View>
</View> </View>

View file

@ -6,7 +6,6 @@ import {
ScrollView, ScrollView,
SafeAreaView, SafeAreaView,
Platform, Platform,
TouchableOpacity,
StatusBar, StatusBar,
Switch, Switch,
} from 'react-native'; } from 'react-native';
@ -15,6 +14,7 @@ import { useSettings, AppSettings } from '../hooks/useSettings';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@ -38,7 +38,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
onPress={onPress} onPress={onPress}
activeOpacity={0.7} activeOpacity={0.7}
style={[ style={[
@ -87,7 +87,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
/> />
)} )}
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -173,7 +173,7 @@ const PlayerSettingsScreen: React.FC = () => {
/> />
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={handleBack} onPress={handleBack}
style={styles.backButton} style={styles.backButton}
activeOpacity={0.7} activeOpacity={0.7}
@ -186,7 +186,7 @@ const PlayerSettingsScreen: React.FC = () => {
<Text style={[styles.backText, { color: currentTheme.colors.text }]}> <Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings Settings
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.headerActions}> <View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */} {/* Empty for now, but ready for future actions */}

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
Switch, Switch,
TextInput, TextInput,
ScrollView, ScrollView,
@ -25,6 +24,7 @@ import { useSettings } from '../hooks/useSettings';
import { localScraperService, pluginService, ScraperInfo, RepositoryInfo } from '../services/pluginService'; import { localScraperService, pluginService, ScraperInfo, RepositoryInfo } from '../services/pluginService';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width: screenWidth } = Dimensions.get('window'); const { width: screenWidth } = Dimensions.get('window');
@ -773,23 +773,23 @@ const CollapsibleSection: React.FC<{
styles: any; styles: any;
}> = ({ title, children, isExpanded, onToggle, colors, styles }) => ( }> = ({ title, children, isExpanded, onToggle, colors, styles }) => (
<View style={styles.collapsibleSection}> <View style={styles.collapsibleSection}>
<TouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}> <FocusableTouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
<Text style={styles.collapsibleTitle}>{title}</Text> <Text style={styles.collapsibleTitle}>{title}</Text>
<Ionicons <Ionicons
name={isExpanded ? "chevron-up" : "chevron-down"} name={isExpanded ? "chevron-up" : "chevron-down"}
size={20} size={20}
color={colors.mediumGray} color={colors.mediumGray}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
{isExpanded && <View style={styles.collapsibleContent}>{children}</View>} {isExpanded && <View style={styles.collapsibleContent}>{children}</View>}
</View> </View>
); );
// Helper component for info tooltips // Helper component for info tooltips
const InfoTooltip: React.FC<{ text: string; colors: any }> = ({ text, colors }) => ( const InfoTooltip: React.FC<{ text: string; colors: any }> = ({ text, colors }) => (
<TouchableOpacity style={{ marginLeft: 8 }}> <View style={{ marginLeft: 8 }}>
<Ionicons name="information-circle-outline" size={16} color={colors.mediumGray} /> <Ionicons name="information-circle-outline" size={16} color={colors.mediumGray} />
</TouchableOpacity> </View>
); );
// Helper component for status badges // Helper component for status badges
@ -1361,22 +1361,22 @@ const PluginsScreen: React.FC = () => {
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<Ionicons name="arrow-back" size={24} color={colors.primary} /> <Ionicons name="arrow-back" size={24} color={colors.primary} />
<Text style={styles.backText}>Settings</Text> <Text style={styles.backText}>Settings</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.headerActions}> <View style={styles.headerActions}>
{/* Help Button */} {/* Help Button */}
<TouchableOpacity <FocusableTouchableOpacity
style={styles.headerButton} style={styles.headerButton}
onPress={() => setShowHelpModal(true)} onPress={() => setShowHelpModal(true)}
> >
<Ionicons name="help-circle-outline" size={20} color={colors.primary} /> <Ionicons name="help-circle-outline" size={20} color={colors.primary} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
@ -1485,7 +1485,7 @@ const PluginsScreen: React.FC = () => {
</View> </View>
<View style={styles.repositoryActions}> <View style={styles.repositoryActions}>
{repo.id !== currentRepositoryId && ( {repo.id !== currentRepositoryId && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.repositoryActionButton, styles.repositoryActionButtonPrimary]} style={[styles.repositoryActionButton, styles.repositoryActionButtonPrimary]}
onPress={() => handleSwitchRepository(repo.id)} onPress={() => handleSwitchRepository(repo.id)}
disabled={switchingRepository === repo.id} disabled={switchingRepository === repo.id}
@ -1495,9 +1495,9 @@ const PluginsScreen: React.FC = () => {
) : ( ) : (
<Text style={styles.repositoryActionButtonText}>Switch</Text> <Text style={styles.repositoryActionButtonText}>Switch</Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.repositoryActionButton, styles.repositoryActionButtonSecondary]} style={[styles.repositoryActionButton, styles.repositoryActionButtonSecondary]}
onPress={() => handleRefreshRepository()} onPress={() => handleRefreshRepository()}
disabled={isRefreshing || switchingRepository !== null} disabled={isRefreshing || switchingRepository !== null}
@ -1507,14 +1507,14 @@ const PluginsScreen: React.FC = () => {
) : ( ) : (
<Text style={styles.repositoryActionButtonText}>Refresh</Text> <Text style={styles.repositoryActionButtonText}>Refresh</Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.repositoryActionButton, styles.repositoryActionButtonDanger]} style={[styles.repositoryActionButton, styles.repositoryActionButtonDanger]}
onPress={() => handleRemoveRepository(repo.id)} onPress={() => handleRemoveRepository(repo.id)}
disabled={switchingRepository !== null} disabled={switchingRepository !== null}
> >
<Text style={styles.repositoryActionButtonText}>Remove</Text> <Text style={styles.repositoryActionButtonText}>Remove</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
))} ))}
@ -1523,13 +1523,13 @@ const PluginsScreen: React.FC = () => {
{/* Add Repository Button */} {/* Add Repository Button */}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.button, styles.primaryButton, { marginTop: 16 }]} style={[styles.button, styles.primaryButton, { marginTop: 16 }]}
onPress={() => setShowAddRepositoryModal(true)} onPress={() => setShowAddRepositoryModal(true)}
disabled={!settings.enableLocalScrapers || switchingRepository !== null} disabled={!settings.enableLocalScrapers || switchingRepository !== null}
> >
<Text style={styles.buttonText}>Add New Repository</Text> <Text style={styles.buttonText}>Add New Repository</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</CollapsibleSection> </CollapsibleSection>
{/* Available Plugins */} {/* Available Plugins */}
@ -1553,16 +1553,16 @@ const PluginsScreen: React.FC = () => {
placeholderTextColor={colors.mediumGray} placeholderTextColor={colors.mediumGray}
/> />
{searchQuery.length > 0 && ( {searchQuery.length > 0 && (
<TouchableOpacity onPress={() => setSearchQuery('')}> <FocusableTouchableOpacity onPress={() => setSearchQuery('')}>
<Ionicons name="close-circle" size={20} color={colors.mediumGray} /> <Ionicons name="close-circle" size={20} color={colors.mediumGray} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
{/* Filter Chips */} {/* Filter Chips */}
<View style={styles.filterContainer}> <View style={styles.filterContainer}>
{['all', 'movie', 'tv'].map((filter) => ( {['all', 'movie', 'tv'].map((filter) => (
<TouchableOpacity <FocusableTouchableOpacity
key={filter} key={filter}
style={[ style={[
styles.filterChip, styles.filterChip,
@ -1576,27 +1576,27 @@ const PluginsScreen: React.FC = () => {
]}> ]}>
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'} {filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</View> </View>
{/* Bulk Actions */} {/* Bulk Actions */}
{filteredScrapers.length > 0 && ( {filteredScrapers.length > 0 && (
<View style={styles.bulkActionsContainer}> <View style={styles.bulkActionsContainer}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.bulkActionButton, styles.bulkActionButtonEnabled]} style={[styles.bulkActionButton, styles.bulkActionButtonEnabled]}
onPress={() => handleBulkToggle(true)} onPress={() => handleBulkToggle(true)}
disabled={isRefreshing} disabled={isRefreshing}
> >
<Text style={[styles.bulkActionButtonText, { color: '#34C759' }]}>Enable All</Text> <Text style={[styles.bulkActionButtonText, { color: '#34C759' }]}>Enable All</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.bulkActionButton, styles.bulkActionButtonDisabled]} style={[styles.bulkActionButton, styles.bulkActionButtonDisabled]}
onPress={() => handleBulkToggle(false)} onPress={() => handleBulkToggle(false)}
disabled={isRefreshing} disabled={isRefreshing}
> >
<Text style={[styles.bulkActionButtonText, { color: colors.mediumGray }]}>Disable All</Text> <Text style={[styles.bulkActionButtonText, { color: colors.mediumGray }]}>Disable All</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}
</> </>
@ -1620,12 +1620,12 @@ const PluginsScreen: React.FC = () => {
} }
</Text> </Text>
{searchQuery && ( {searchQuery && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.button, styles.secondaryButton]} style={[styles.button, styles.secondaryButton]}
onPress={() => setSearchQuery('')} onPress={() => setSearchQuery('')}
> >
<Text style={styles.secondaryButtonText}>Clear Search</Text> <Text style={styles.secondaryButtonText}>Clear Search</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
) : ( ) : (
@ -1713,14 +1713,14 @@ const PluginsScreen: React.FC = () => {
numberOfLines={1} numberOfLines={1}
/> />
{showboxSavedToken.length > 0 && ( {showboxSavedToken.length > 0 && (
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}> <FocusableTouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} /> <Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
<View style={styles.buttonRow}> <View style={styles.buttonRow}>
{showboxUiToken !== showboxSavedToken && ( {showboxUiToken !== showboxSavedToken && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.button, styles.primaryButton]} style={[styles.button, styles.primaryButton]}
onPress={async () => { onPress={async () => {
if (showboxScraperId) { if (showboxScraperId) {
@ -1731,9 +1731,9 @@ const PluginsScreen: React.FC = () => {
}} }}
> >
<Text style={styles.buttonText}>Save</Text> <Text style={styles.buttonText}>Save</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.button, styles.secondaryButton]} style={[styles.button, styles.secondaryButton]}
onPress={async () => { onPress={async () => {
setShowboxUiToken(''); setShowboxUiToken('');
@ -1744,7 +1744,7 @@ const PluginsScreen: React.FC = () => {
}} }}
> >
<Text style={styles.secondaryButtonText}>Clear</Text> <Text style={styles.secondaryButtonText}>Clear</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
)} )}
@ -1849,7 +1849,7 @@ const PluginsScreen: React.FC = () => {
{qualityOptions.map((quality) => { {qualityOptions.map((quality) => {
const isExcluded = (settings.excludedQualities || []).includes(quality); const isExcluded = (settings.excludedQualities || []).includes(quality);
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={quality} key={quality}
style={[ style={[
styles.qualityChip, styles.qualityChip,
@ -1866,7 +1866,7 @@ const PluginsScreen: React.FC = () => {
]}> ]}>
{isExcluded ? '✕ ' : ''}{quality} {isExcluded ? '✕ ' : ''}{quality}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
})} })}
</View> </View>
@ -1898,7 +1898,7 @@ const PluginsScreen: React.FC = () => {
{languageOptions.map((language) => { {languageOptions.map((language) => {
const isExcluded = (settings.excludedLanguages || []).includes(language); const isExcluded = (settings.excludedLanguages || []).includes(language);
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={language} key={language}
style={[ style={[
styles.qualityChip, styles.qualityChip,
@ -1915,7 +1915,7 @@ const PluginsScreen: React.FC = () => {
]}> ]}>
{isExcluded ? '✕ ' : ''}{language} {isExcluded ? '✕ ' : ''}{language}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
})} })}
</View> </View>
@ -1964,12 +1964,12 @@ const PluginsScreen: React.FC = () => {
<Text style={styles.modalText}> <Text style={styles.modalText}>
4. <Text style={{ fontWeight: '600' }}>Enable Scrapers</Text> - Turn on the scrapers you want to use for streaming 4. <Text style={{ fontWeight: '600' }}>Enable Scrapers</Text> - Turn on the scrapers you want to use for streaming
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.modalButton} style={styles.modalButton}
onPress={() => setShowHelpModal(false)} onPress={() => setShowHelpModal(false)}
> >
<Text style={styles.modalButtonText}>Got it!</Text> <Text style={styles.modalButtonText}>Got it!</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
</Modal> </Modal>
@ -2011,7 +2011,7 @@ const PluginsScreen: React.FC = () => {
{/* Action Buttons */} {/* Action Buttons */}
<View style={styles.compactActions}> <View style={styles.compactActions}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.compactButton, styles.cancelButton]} style={[styles.compactButton, styles.cancelButton]}
onPress={() => { onPress={() => {
setShowAddRepositoryModal(false); setShowAddRepositoryModal(false);
@ -2019,9 +2019,9 @@ const PluginsScreen: React.FC = () => {
}} }}
> >
<Text style={styles.cancelButtonText}>Cancel</Text> <Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]} style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
onPress={handleAddRepository} onPress={handleAddRepository}
disabled={!newRepositoryUrl.trim() || isLoading} disabled={!newRepositoryUrl.trim() || isLoading}
@ -2031,7 +2031,7 @@ const PluginsScreen: React.FC = () => {
) : ( ) : (
<Text style={styles.addButtonText}>Add</Text> <Text style={styles.addButtonText}>Add</Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</ScrollView> </ScrollView>
</View> </View>

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
FlatList, FlatList,
StatusBar, StatusBar,
Platform, Platform,
@ -17,6 +16,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useTraktContext } from '../contexts/TraktContext'; import { useTraktContext } from '../contexts/TraktContext';
import { mmkvStorage } from '../services/mmkvStorage'; import { mmkvStorage } from '../services/mmkvStorage';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const PROFILE_STORAGE_KEY = 'user_profiles'; const PROFILE_STORAGE_KEY = 'user_profiles';
@ -183,7 +183,7 @@ const ProfilesScreen: React.FC = () => {
const renderItem = ({ item }: { item: Profile }) => ( const renderItem = ({ item }: { item: Profile }) => (
<View style={styles.profileItem}> <View style={styles.profileItem}>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.profileContent, styles.profileContent,
item.isActive && { item.isActive && {
@ -211,14 +211,14 @@ const ProfilesScreen: React.FC = () => {
)} )}
</View> </View>
{!item.isActive && ( {!item.isActive && (
<TouchableOpacity <FocusableTouchableOpacity
style={styles.deleteButton} style={styles.deleteButton}
onPress={() => handleDeleteProfile(item.id)} onPress={() => handleDeleteProfile(item.id)}
> >
<MaterialIcons name="delete" size={24} color={currentTheme.colors.error} /> <MaterialIcons name="delete" size={24} color={currentTheme.colors.error} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
); );
@ -227,7 +227,7 @@ const ProfilesScreen: React.FC = () => {
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent /> <StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={handleBack} onPress={handleBack}
style={styles.backButton} style={styles.backButton}
activeOpacity={0.7} activeOpacity={0.7}
@ -237,7 +237,7 @@ const ProfilesScreen: React.FC = () => {
size={24} size={24}
color={currentTheme.colors.text} color={currentTheme.colors.text}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
<Text <Text
style={[ style={[
styles.headerTitle, styles.headerTitle,
@ -260,7 +260,7 @@ const ProfilesScreen: React.FC = () => {
</Text> </Text>
} }
ListFooterComponent={ ListFooterComponent={
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.addButton, styles.addButton,
{ backgroundColor: currentTheme.colors.elevation2 } { backgroundColor: currentTheme.colors.elevation2 }
@ -271,7 +271,7 @@ const ProfilesScreen: React.FC = () => {
<Text style={[styles.addButtonText, { color: currentTheme.colors.text }]}> <Text style={[styles.addButtonText, { color: currentTheme.colors.text }]}>
Add New Profile Add New Profile
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
} }
/> />
</View> </View>
@ -307,7 +307,7 @@ const ProfilesScreen: React.FC = () => {
/> />
<View style={styles.modalButtons}> <View style={styles.modalButtons}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.modalButton, styles.cancelButton]} style={[styles.modalButton, styles.cancelButton]}
onPress={() => { onPress={() => {
setNewProfileName(''); setNewProfileName('');
@ -315,8 +315,8 @@ const ProfilesScreen: React.FC = () => {
}} }}
> >
<Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text> <Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.modalButton, styles.modalButton,
styles.createButton, styles.createButton,
@ -325,7 +325,7 @@ const ProfilesScreen: React.FC = () => {
onPress={handleAddProfile} onPress={handleAddProfile}
> >
<Text style={{ color: '#fff' }}>Create</Text> <Text style={{ color: '#fff' }}>Create</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
</View> </View>

View file

@ -5,7 +5,6 @@ import {
StyleSheet, StyleSheet,
TextInput, TextInput,
FlatList, FlatList,
TouchableOpacity,
ActivityIndicator, ActivityIndicator,
useColorScheme, useColorScheme,
SafeAreaView, SafeAreaView,
@ -44,6 +43,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import LoadingSpinner from '../components/common/LoadingSpinner'; import LoadingSpinner from '../components/common/LoadingSpinner';
import ScreenHeader from '../components/common/ScreenHeader'; import ScreenHeader from '../components/common/ScreenHeader';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window'); 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 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 SkeletonLoader = () => {
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current; const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
@ -570,24 +570,29 @@ const SearchScreen = () => {
Recent Searches Recent Searches
</Text> </Text>
{recentSearches.map((search, index) => ( {recentSearches.map((search, index) => (
<TouchableOpacity <View key={index} style={styles.recentSearchItem}>
key={index}
style={styles.recentSearchItem}
onPress={() => {
setQuery(search);
Keyboard.dismiss();
}}
>
<MaterialIcons <MaterialIcons
name="history" name="history"
size={20} size={20}
color={currentTheme.colors.lightGray} color={currentTheme.colors.lightGray}
style={styles.recentSearchIcon} style={styles.recentSearchIcon}
/> />
<Text style={[styles.recentSearchText, { color: currentTheme.colors.white }]}> <FocusableTouchableOpacity
{search} style={{ flex: 1 }}
</Text> onPress={() => {
<TouchableOpacity setQuery(search);
Keyboard.dismiss();
}}
activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={12}
>
<Text style={[styles.recentSearchText, { color: currentTheme.colors.white }]}>
{search}
</Text>
</FocusableTouchableOpacity>
<FocusableTouchableOpacity
onPress={() => { onPress={() => {
const newRecentSearches = [...recentSearches]; const newRecentSearches = [...recentSearches];
newRecentSearches.splice(index, 1); newRecentSearches.splice(index, 1);
@ -596,10 +601,15 @@ const SearchScreen = () => {
}} }}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
style={styles.recentSearchDeleteButton} style={styles.recentSearchDeleteButton}
activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={12}
hasTVPreferredFocus={Platform.isTV && index === 0}
> >
<MaterialIcons name="close" size={16} color={currentTheme.colors.lightGray} /> <MaterialIcons name="close" size={16} color={currentTheme.colors.lightGray} />
</TouchableOpacity> </FocusableTouchableOpacity>
</TouchableOpacity> </View>
))} ))}
</View> </View>
); );
@ -651,7 +661,7 @@ const SearchScreen = () => {
}, [item.id, item.type]); }, [item.id, item.type]);
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.horizontalItem, { width: itemWidth }]} style={[styles.horizontalItem, { width: itemWidth }]}
onPress={() => { onPress={() => {
navigation.navigate('Metadata', { id: item.id, type: item.type }); navigation.navigate('Metadata', { id: item.id, type: item.type });
@ -663,6 +673,9 @@ const SearchScreen = () => {
}} }}
delayLongPress={300} delayLongPress={300}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="poster"
focusBorderRadius={12}
> >
<View style={[styles.horizontalItemPosterContainer, { <View style={[styles.horizontalItemPosterContainer, {
width: itemWidth, width: itemWidth,
@ -716,7 +729,7 @@ const SearchScreen = () => {
{item.year} {item.year}
</Text> </Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -934,17 +947,21 @@ const SearchScreen = () => {
ref={inputRef} ref={inputRef}
/> />
{query.length > 0 && ( {query.length > 0 && (
<TouchableOpacity <FocusableTouchableOpacity
onPress={handleClearSearch} onPress={handleClearSearch}
style={styles.clearButton} style={styles.clearButton}
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }} hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="icon"
focusBorderRadius={999}
> >
<MaterialIcons <MaterialIcons
name="close" name="close"
size={20} size={20}
color={currentTheme.colors.lightGray} color={currentTheme.colors.lightGray}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
</View> </View>

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
Switch, Switch,
ScrollView, ScrollView,
SafeAreaView, SafeAreaView,
@ -56,6 +55,7 @@ import TraktIcon from '../components/icons/TraktIcon';
import TMDBIcon from '../components/icons/TMDBIcon'; import TMDBIcon from '../components/icons/TMDBIcon';
import MDBListIcon from '../components/icons/MDBListIcon'; import MDBListIcon from '../components/icons/MDBListIcon';
import { campaignService } from '../services/campaignService'; import { campaignService } from '../services/campaignService';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
const isTablet = width >= 768; const isTablet = width >= 768;
@ -144,7 +144,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
activeOpacity={0.6} activeOpacity={0.6}
onPress={onPress} onPress={onPress}
style={[ style={[
@ -153,6 +153,9 @@ const SettingItem: React.FC<SettingItemProps> = ({
{ borderBottomColor: currentTheme.colors.elevation2 }, { borderBottomColor: currentTheme.colors.elevation2 },
isTablet && styles.tabletSettingItem isTablet && styles.tabletSettingItem
]} ]}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={isTablet ? 18 : 16}
> >
<View style={[ <View style={[
styles.settingIconContainer, styles.settingIconContainer,
@ -201,7 +204,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
{renderControl()} {renderControl()}
</View> </View>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -237,7 +240,7 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}> <ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
{categories.map((category) => ( {categories.map((category) => (
<TouchableOpacity <FocusableTouchableOpacity
key={category.id} key={category.id}
style={[ style={[
styles.sidebarItem, styles.sidebarItem,
@ -248,6 +251,10 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
]} ]}
onPress={() => onCategorySelect(category.id)} onPress={() => onCategorySelect(category.id)}
activeOpacity={0.6} activeOpacity={0.6}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={14}
hasTVPreferredFocus={Platform.isTV && selectedCategory === category.id}
> >
<View style={[ <View style={[
styles.sidebarItemIconContainer, styles.sidebarItemIconContainer,
@ -278,7 +285,7 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
]}> ]}>
{category.title} {category.title}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</ScrollView> </ScrollView>
</View> </View>
@ -959,7 +966,7 @@ const SettingsScreen: React.FC = () => {
)} )}
<View style={styles.discordContainer}> <View style={styles.discordContainer}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]} style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
onPress={() => { onPress={() => {
const url = 'https://ko-fi.com/tapframe'; const url = 'https://ko-fi.com/tapframe';
@ -973,19 +980,25 @@ const SettingsScreen: React.FC = () => {
} }
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<FastImage <FastImage
source={require('../../assets/support_me_on_kofi_red.png')} source={require('../../assets/support_me_on_kofi_red.png')}
style={styles.kofiImage} style={styles.kofiImage}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}> <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]} style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]}
onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')} onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<View style={styles.discordButtonContent}> <View style={styles.discordButtonContent}>
<FastImage <FastImage
@ -997,12 +1010,15 @@ const SettingsScreen: React.FC = () => {
Discord Discord
</Text> </Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]} style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]}
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')} onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<View style={styles.discordButtonContent}> <View style={styles.discordButtonContent}>
<FastImage <FastImage
@ -1014,7 +1030,7 @@ const SettingsScreen: React.FC = () => {
Reddit Reddit
</Text> </Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
@ -1093,7 +1109,7 @@ const SettingsScreen: React.FC = () => {
{/* Support & Community Buttons */} {/* Support & Community Buttons */}
<View style={styles.discordContainer}> <View style={styles.discordContainer}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]} style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
onPress={() => { onPress={() => {
const url = 'https://ko-fi.com/tapframe'; const url = 'https://ko-fi.com/tapframe';
@ -1107,19 +1123,25 @@ const SettingsScreen: React.FC = () => {
} }
}} }}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<FastImage <FastImage
source={require('../../assets/support_me_on_kofi_red.png')} source={require('../../assets/support_me_on_kofi_red.png')}
style={styles.kofiImage} style={styles.kofiImage}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
/> />
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}> <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]} style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]}
onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')} onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<View style={styles.discordButtonContent}> <View style={styles.discordButtonContent}>
<FastImage <FastImage
@ -1131,12 +1153,15 @@ const SettingsScreen: React.FC = () => {
Discord Discord
</Text> </Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]} style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]}
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')} onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={14}
> >
<View style={styles.discordButtonContent}> <View style={styles.discordButtonContent}>
<FastImage <FastImage
@ -1148,7 +1173,7 @@ const SettingsScreen: React.FC = () => {
Reddit Reddit
</Text> </Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>

View file

@ -6,7 +6,6 @@ import {
ScrollView, ScrollView,
ActivityIndicator, ActivityIndicator,
SafeAreaView, SafeAreaView,
TouchableOpacity,
Platform, Platform,
StatusBar, StatusBar,
} from 'react-native'; } from 'react-native';
@ -34,6 +33,7 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import axios from 'axios'; import axios from 'axios';
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated'; import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
type RootStackParamList = { type RootStackParamList = {
ShowRatings: { showId: number }; ShowRatings: { showId: number };
@ -140,7 +140,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
{['tmdb', 'imdb', 'tvmaze'].map((source) => { {['tmdb', 'imdb', 'tvmaze'].map((source) => {
const isActive = ratingSource === source; const isActive = ratingSource === source;
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
key={source} key={source}
style={[ style={[
styles.sourceButton, styles.sourceButton,
@ -148,6 +148,10 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
isActive && { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary } isActive && { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary }
]} ]}
onPress={() => setRatingSource(source as RatingSource)} onPress={() => setRatingSource(source as RatingSource)}
enableTVFocus={Platform.isTV}
preset="pill"
focusBorderRadius={8}
hasTVPreferredFocus={Platform.isTV && isActive}
> >
<Text <Text
style={{ style={{
@ -158,7 +162,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
> >
{source.toUpperCase()} {source.toUpperCase()}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
})} })}
</View> </View>

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
SectionList, SectionList,
@ -58,6 +57,7 @@ import StreamCard from '../components/StreamCard';
import AnimatedImage from '../components/AnimatedImage'; import AnimatedImage from '../components/AnimatedImage';
import AnimatedText from '../components/AnimatedText'; import AnimatedText from '../components/AnimatedText';
import AnimatedView from '../components/AnimatedView'; import AnimatedView from '../components/AnimatedView';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Lazy-safe community blur import for Android // Lazy-safe community blur import for Android
let AndroidBlurView: any = null; let AndroidBlurView: any = null;
@ -1850,19 +1850,23 @@ export const StreamsScreen = () => {
<View <View
style={[styles.backButtonContainer, isTablet && styles.backButtonContainerTablet]} style={[styles.backButtonContainer, isTablet && styles.backButtonContainerTablet]}
> >
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.backButton, styles.backButton,
Platform.OS === 'android' ? { paddingTop: 45 } : null Platform.OS === 'android' ? { paddingTop: 45 } : null
]} ]}
onPress={handleBack} onPress={handleBack}
activeOpacity={0.7} activeOpacity={0.7}
enableTVFocus={Platform.isTV}
preset="listRow"
focusBorderRadius={999}
hasTVPreferredFocus={Platform.isTV}
> >
<MaterialIcons name="arrow-back" size={24} color={colors.white} /> <MaterialIcons name="arrow-back" size={24} color={colors.white} />
<Text style={styles.backButtonText}> <Text style={styles.backButtonText}>
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? 'Back to Episodes' : 'Back to Info'} {metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? 'Back to Episodes' : 'Back to Info'}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}
@ -2105,12 +2109,16 @@ export const StreamsScreen = () => {
<Text style={styles.noStreamsSubText}> <Text style={styles.noStreamsSubText}>
Please add streaming sources in settings Please add streaming sources in settings
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.addSourcesButton} style={styles.addSourcesButton}
onPress={() => navigation.navigate('Addons')} onPress={() => navigation.navigate('Addons')}
enableTVFocus={Platform.isTV}
preset="button"
focusBorderRadius={16}
hasTVPreferredFocus={Platform.isTV}
> >
<Text style={styles.addSourcesButtonText}>Add Sources</Text> <Text style={styles.addSourcesButtonText}>Add Sources</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
) : streamsEmpty ? ( ) : streamsEmpty ? (
showInitialLoading ? ( showInitialLoading ? (

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
TextInput, TextInput,
SafeAreaView, SafeAreaView,
StatusBar, StatusBar,
@ -29,6 +28,7 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
// (duplicate import removed) // (duplicate import removed)
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key'; const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key'; const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
@ -516,13 +516,13 @@ const TMDBSettingsScreen = () => {
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}> <View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} /> <MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text> <Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}> <Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
TMDb Settings TMDb Settings
@ -592,12 +592,12 @@ const TMDBSettingsScreen = () => {
Current: {(settings.tmdbLanguagePreference || 'en').toUpperCase()} Current: {(settings.tmdbLanguagePreference || 'en').toUpperCase()}
</Text> </Text>
</View> </View>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => setLanguagePickerVisible(true)} onPress={() => setLanguagePickerVisible(true)}
style={[styles.languageButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.languageButton, { backgroundColor: currentTheme.colors.primary }]}
> >
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text> <Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
{/* Logo Preview */} {/* Logo Preview */}
@ -617,7 +617,7 @@ const TMDBSettingsScreen = () => {
style={styles.showsScrollView} style={styles.showsScrollView}
> >
{EXAMPLE_SHOWS.map((show) => ( {EXAMPLE_SHOWS.map((show) => (
<TouchableOpacity <FocusableTouchableOpacity
key={show.imdbId} key={show.imdbId}
style={[ style={[
styles.showItem, styles.showItem,
@ -636,7 +636,7 @@ const TMDBSettingsScreen = () => {
> >
{show.name} {show.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</ScrollView> </ScrollView>
@ -725,29 +725,29 @@ const TMDBSettingsScreen = () => {
onFocus={() => setIsInputFocused(true)} onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)} onBlur={() => setIsInputFocused(false)}
/> />
<TouchableOpacity <FocusableTouchableOpacity
style={styles.pasteButton} style={styles.pasteButton}
onPress={pasteFromClipboard} onPress={pasteFromClipboard}
> >
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} /> <MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<View style={styles.buttonRow}> <View style={styles.buttonRow}>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]} style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
onPress={saveApiKey} onPress={saveApiKey}
> >
<Text style={[styles.buttonText, { color: currentTheme.colors.white }]}>Save</Text> <Text style={[styles.buttonText, { color: currentTheme.colors.white }]}>Save</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
{isKeySet && ( {isKeySet && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.button, styles.clearButton, { borderColor: currentTheme.colors.error }]} style={[styles.button, styles.clearButton, { borderColor: currentTheme.colors.error }]}
onPress={clearApiKey} onPress={clearApiKey}
> >
<Text style={[styles.buttonText, { color: currentTheme.colors.error }]}>Clear</Text> <Text style={[styles.buttonText, { color: currentTheme.colors.error }]}>Clear</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
@ -771,7 +771,7 @@ const TMDBSettingsScreen = () => {
</View> </View>
)} )}
<TouchableOpacity <FocusableTouchableOpacity
style={styles.helpLink} style={styles.helpLink}
onPress={openTMDBWebsite} onPress={openTMDBWebsite}
> >
@ -779,7 +779,7 @@ const TMDBSettingsScreen = () => {
<Text style={[styles.helpText, { color: currentTheme.colors.primary }]}> <Text style={[styles.helpText, { color: currentTheme.colors.primary }]}>
How to get a TMDb API key? How to get a TMDb API key?
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</> </>
)} )}
@ -805,7 +805,7 @@ const TMDBSettingsScreen = () => {
</View> </View>
</View> </View>
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.button, { backgroundColor: currentTheme.colors.error }]} style={[styles.button, { backgroundColor: currentTheme.colors.error }]}
onPress={handleClearCache} onPress={handleClearCache}
> >
@ -813,7 +813,7 @@ const TMDBSettingsScreen = () => {
<MaterialIcons name="delete-outline" size={18} color={currentTheme.colors.white} /> <MaterialIcons name="delete-outline" size={18} color={currentTheme.colors.white} />
<Text style={[styles.buttonText, { color: currentTheme.colors.white, marginLeft: 8 }]}>Clear Cache</Text> <Text style={[styles.buttonText, { color: currentTheme.colors.white, marginLeft: 8 }]}>Clear Cache</Text>
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={[styles.infoContainer, { marginTop: 12 }]}> <View style={[styles.infoContainer, { marginTop: 12 }]}>
<MaterialIcons name="info-outline" size={18} color={currentTheme.colors.primary} /> <MaterialIcons name="info-outline" size={18} color={currentTheme.colors.primary} />
@ -856,9 +856,9 @@ const TMDBSettingsScreen = () => {
autoCorrect={false} autoCorrect={false}
/> />
{languageSearch.length > 0 && ( {languageSearch.length > 0 && (
<TouchableOpacity onPress={() => setLanguageSearch('')} style={styles.searchClearButton}> <FocusableTouchableOpacity onPress={() => setLanguageSearch('')} style={styles.searchClearButton}>
<MaterialIcons name="close" size={20} color={currentTheme.colors.mediumEmphasis} /> <MaterialIcons name="close" size={20} color={currentTheme.colors.mediumEmphasis} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
</View> </View>
@ -880,7 +880,7 @@ const TMDBSettingsScreen = () => {
{ code: 'de', label: 'DE' }, { code: 'de', label: 'DE' },
{ code: 'tr', label: 'TR' }, { code: 'tr', label: 'TR' },
].map(({ code, label }) => ( ].map(({ code, label }) => (
<TouchableOpacity <FocusableTouchableOpacity
key={code} key={code}
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }} onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
style={[ style={[
@ -899,7 +899,7 @@ const TMDBSettingsScreen = () => {
]}> ]}>
{label} {label}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
</ScrollView> </ScrollView>
</View> </View>
@ -956,7 +956,7 @@ const TMDBSettingsScreen = () => {
return ( return (
<> <>
{filteredLanguages.map(({ code, label, native }) => ( {filteredLanguages.map(({ code, label, native }) => (
<TouchableOpacity <FocusableTouchableOpacity
key={code} key={code}
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }} onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
style={[ style={[
@ -992,7 +992,7 @@ const TMDBSettingsScreen = () => {
</View> </View>
)} )}
</View> </View>
</TouchableOpacity> </FocusableTouchableOpacity>
))} ))}
{languageSearch.length > 0 && filteredLanguages.length === 0 && ( {languageSearch.length > 0 && filteredLanguages.length === 0 && (
<View style={styles.noResultsContainer}> <View style={styles.noResultsContainer}>
@ -1000,12 +1000,12 @@ const TMDBSettingsScreen = () => {
<Text style={[styles.noResultsText, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.noResultsText, { color: currentTheme.colors.mediumEmphasis }]}>
No languages found for "{languageSearch}" No languages found for "{languageSearch}"
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => setLanguageSearch('')} onPress={() => setLanguageSearch('')}
style={[styles.clearSearchButton, { backgroundColor: currentTheme.colors.elevation1 }]} style={[styles.clearSearchButton, { backgroundColor: currentTheme.colors.elevation1 }]}
> >
<Text style={[styles.clearSearchButtonText, { color: currentTheme.colors.primary }]}>Clear search</Text> <Text style={[styles.clearSearchButtonText, { color: currentTheme.colors.primary }]}>Clear search</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}
</> </>
@ -1016,18 +1016,18 @@ const TMDBSettingsScreen = () => {
{/* Footer Actions */} {/* Footer Actions */}
<View style={styles.modalFooter}> <View style={styles.modalFooter}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => setLanguagePickerVisible(false)} onPress={() => setLanguagePickerVisible(false)}
style={styles.cancelButton} style={styles.cancelButton}
> >
<Text style={[styles.cancelButtonText, { color: currentTheme.colors.text }]}>Cancel</Text> <Text style={[styles.cancelButtonText, { color: currentTheme.colors.text }]}>Cancel</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => setLanguagePickerVisible(false)} onPress={() => setLanguagePickerVisible(false)}
style={[styles.doneButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.doneButton, { backgroundColor: currentTheme.colors.primary }]}
> >
<Text style={[styles.doneButtonText, { color: currentTheme.colors.white }]}>Done</Text> <Text style={[styles.doneButtonText, { color: currentTheme.colors.white }]}>Done</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
Switch, Switch,
ScrollView, ScrollView,
Platform, Platform,
@ -24,6 +23,7 @@ import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext';
import { RootStackParamList } from '../navigation/AppNavigator'; import { RootStackParamList } from '../navigation/AppNavigator';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
const { width } = Dimensions.get('window'); const { width } = Dimensions.get('window');
@ -53,7 +53,7 @@ const ThemeCard: React.FC<ThemeCardProps> = ({
onDelete onDelete
}) => { }) => {
return ( return (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.themeCard, styles.themeCard,
isSelected && styles.selectedThemeCard, isSelected && styles.selectedThemeCard,
@ -85,24 +85,24 @@ const ThemeCard: React.FC<ThemeCardProps> = ({
{theme.isEditable && ( {theme.isEditable && (
<View style={styles.themeCardActions}> <View style={styles.themeCardActions}>
{onEdit && ( {onEdit && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.themeCardAction, styles.buttonShadow]} style={[styles.themeCardAction, styles.buttonShadow]}
onPress={onEdit} onPress={onEdit}
> >
<MaterialIcons name="edit" size={16} color={theme.colors.primary} /> <MaterialIcons name="edit" size={16} color={theme.colors.primary} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
{onDelete && ( {onDelete && (
<TouchableOpacity <FocusableTouchableOpacity
style={[styles.themeCardAction, styles.buttonShadow]} style={[styles.themeCardAction, styles.buttonShadow]}
onPress={onDelete} onPress={onDelete}
> >
<MaterialIcons name="delete" size={16} color={theme.colors.error} /> <MaterialIcons name="delete" size={16} color={theme.colors.error} />
</TouchableOpacity> </FocusableTouchableOpacity>
)} )}
</View> </View>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
); );
}; };
@ -120,7 +120,7 @@ const FilterTab: React.FC<FilterTabProps> = ({
onPress, onPress,
primaryColor primaryColor
}) => ( }) => (
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.filterTab, styles.filterTab,
isActive && { backgroundColor: primaryColor }, isActive && { backgroundColor: primaryColor },
@ -136,7 +136,7 @@ const FilterTab: React.FC<FilterTabProps> = ({
> >
{category.name} {category.name}
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
); );
type ColorKey = 'primary' | 'secondary' | 'darkBackground'; type ColorKey = 'primary' | 'secondary' | 'darkBackground';
@ -242,12 +242,12 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
return ( return (
<View style={styles.editorContainer}> <View style={styles.editorContainer}>
<View style={styles.editorHeader}> <View style={styles.editorHeader}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.editorBackButton} style={styles.editorBackButton}
onPress={onCancel} onPress={onCancel}
> >
<MaterialIcons name="arrow-back" size={20} color="#FFFFFF" /> <MaterialIcons name="arrow-back" size={20} color="#FFFFFF" />
</TouchableOpacity> </FocusableTouchableOpacity>
<TextInput <TextInput
style={styles.editorTitleInput} style={styles.editorTitleInput}
value={themeName} value={themeName}
@ -255,12 +255,12 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
placeholder="Theme name" placeholder="Theme name"
placeholderTextColor="rgba(255,255,255,0.5)" placeholderTextColor="rgba(255,255,255,0.5)"
/> />
<TouchableOpacity <FocusableTouchableOpacity
style={styles.editorSaveButton} style={styles.editorSaveButton}
onPress={handleSave} onPress={handleSave}
> >
<Text style={styles.saveButtonText}>Save</Text> <Text style={styles.saveButtonText}>Save</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
<View style={styles.editorBody}> <View style={styles.editorBody}>
@ -268,7 +268,7 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
<ThemePreview /> <ThemePreview />
<View style={styles.colorButtonsColumn}> <View style={styles.colorButtonsColumn}>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.colorSelectorButton, styles.colorSelectorButton,
selectedColorKey === 'primary' && styles.selectedColorButton, selectedColorKey === 'primary' && styles.selectedColorButton,
@ -277,9 +277,9 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
onPress={() => setSelectedColorKey('primary')} onPress={() => setSelectedColorKey('primary')}
> >
<Text style={styles.colorButtonText}>Primary</Text> <Text style={styles.colorButtonText}>Primary</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.colorSelectorButton, styles.colorSelectorButton,
selectedColorKey === 'secondary' && styles.selectedColorButton, selectedColorKey === 'secondary' && styles.selectedColorButton,
@ -288,9 +288,9 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
onPress={() => setSelectedColorKey('secondary')} onPress={() => setSelectedColorKey('secondary')}
> >
<Text style={styles.colorButtonText}>Secondary</Text> <Text style={styles.colorButtonText}>Secondary</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.colorSelectorButton, styles.colorSelectorButton,
selectedColorKey === 'darkBackground' && styles.selectedColorButton, selectedColorKey === 'darkBackground' && styles.selectedColorButton,
@ -299,7 +299,7 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
onPress={() => setSelectedColorKey('darkBackground')} onPress={() => setSelectedColorKey('darkBackground')}
> >
<Text style={styles.colorButtonText}>Background</Text> <Text style={styles.colorButtonText}>Background</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
</View> </View>
@ -535,7 +535,7 @@ const ThemeScreen: React.FC = () => {
<StatusBar barStyle="light-content" /> <StatusBar barStyle="light-content" />
<View style={[styles.header, { paddingTop: headerTopPadding }]}> <View style={[styles.header, { paddingTop: headerTopPadding }]}>
<TouchableOpacity <FocusableTouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
> >
@ -543,7 +543,7 @@ const ThemeScreen: React.FC = () => {
<Text style={[styles.backText, { color: currentTheme.colors.text }]}> <Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings Settings
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.headerActions}> <View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */} {/* Empty for now, but ready for future actions */}
@ -595,7 +595,7 @@ const ThemeScreen: React.FC = () => {
))} ))}
</View> </View>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.createButton, styles.createButton,
{ backgroundColor: currentTheme.colors.primary }, { backgroundColor: currentTheme.colors.primary },
@ -605,7 +605,7 @@ const ThemeScreen: React.FC = () => {
> >
<MaterialIcons name="add" size={20} color="#FFFFFF" /> <MaterialIcons name="add" size={20} color="#FFFFFF" />
<Text style={styles.createButtonText}>Create Custom Theme</Text> <Text style={styles.createButtonText}>Create Custom Theme</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.textMuted, marginTop: 24 }]}> <Text style={[styles.sectionTitle, { color: currentTheme.colors.textMuted, marginTop: 24 }]}>
OPTIONS OPTIONS

View file

@ -3,7 +3,6 @@ import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
TouchableOpacity,
ActivityIndicator, ActivityIndicator,
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
@ -24,6 +23,7 @@ import { useTraktIntegration } from '../hooks/useTraktIntegration';
import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings'; import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings';
import { colors } from '../styles'; import { colors } from '../styles';
import CustomAlert from '../components/CustomAlert'; import CustomAlert from '../components/CustomAlert';
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
// Check if running on TV platform // Check if running on TV platform
const isTV = Platform.isTV; const isTV = Platform.isTV;
@ -246,7 +246,7 @@ const TraktSettingsScreen: React.FC = () => {
]}> ]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} /> <StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <FocusableTouchableOpacity
onPress={() => navigation.goBack()} onPress={() => navigation.goBack()}
style={styles.backButton} style={styles.backButton}
> >
@ -258,7 +258,7 @@ const TraktSettingsScreen: React.FC = () => {
<Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}> <Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
Settings Settings
</Text> </Text>
</TouchableOpacity> </FocusableTouchableOpacity>
<View style={styles.headerActions}> <View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */} {/* Empty for now, but ready for future actions */}
@ -328,7 +328,7 @@ const TraktSettingsScreen: React.FC = () => {
</Text> </Text>
</View> </View>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.button, styles.button,
styles.signOutButton, styles.signOutButton,
@ -337,7 +337,7 @@ const TraktSettingsScreen: React.FC = () => {
onPress={handleSignOut} onPress={handleSignOut}
> >
<Text style={styles.buttonText}>Sign Out</Text> <Text style={styles.buttonText}>Sign Out</Text>
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
) : ( ) : (
<View style={styles.signInContainer}> <View style={styles.signInContainer}>
@ -358,7 +358,7 @@ const TraktSettingsScreen: React.FC = () => {
]}> ]}>
Sync your watch history, watchlist, and collection with Trakt.tv Sync your watch history, watchlist, and collection with Trakt.tv
</Text> </Text>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.button, styles.button,
{ backgroundColor: isDarkMode ? currentTheme.colors.primary : currentTheme.colors.primary } { backgroundColor: isDarkMode ? currentTheme.colors.primary : currentTheme.colors.primary }
@ -373,7 +373,7 @@ const TraktSettingsScreen: React.FC = () => {
Sign In with Trakt Sign In with Trakt
</Text> </Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
</View> </View>
)} )}
</View> </View>
@ -448,7 +448,7 @@ const TraktSettingsScreen: React.FC = () => {
</View> </View>
</View> </View>
</View> </View>
<TouchableOpacity <FocusableTouchableOpacity
style={[ style={[
styles.button, styles.button,
{ {
@ -478,7 +478,7 @@ const TraktSettingsScreen: React.FC = () => {
Sync Now Sync Now
</Text> </Text>
)} )}
</TouchableOpacity> </FocusableTouchableOpacity>
{/* Display Settings Section */} {/* Display Settings Section */}
<Text style={[ <Text style={[

21
src/styles/tvFocus.ts Normal file
View file

@ -0,0 +1,21 @@
export type TVFocusPresetName = 'card' | 'poster' | 'pill' | 'button' | 'icon' | 'listRow';
export type TVFocusPreset = {
focusScale: number;
focusRingWidth: number;
/**
* Border radius should be passed by the caller when it is dynamic.
* This is a sensible default.
*/
focusBorderRadius: number;
};
export const tvFocusPresets: Record<TVFocusPresetName, TVFocusPreset> = {
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 },
};