mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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:
parent
18c18257c9
commit
86d3035de6
50 changed files with 1549 additions and 583 deletions
|
|
@ -5,7 +5,6 @@ import {
|
|||
Text,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
|
@ -16,6 +15,7 @@ import Animated, {
|
|||
} from 'react-native-reanimated';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { Portal } from 'react-native-paper';
|
||||
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
|
||||
|
||||
interface CustomAlertProps {
|
||||
visible: boolean;
|
||||
|
|
@ -120,7 +120,7 @@ export const CustomAlert = ({
|
|||
{actions.map((action, idx) => {
|
||||
const isPrimary = idx === actions.length - 1;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={action.label}
|
||||
style={[
|
||||
styles.actionButton,
|
||||
|
|
@ -132,6 +132,10 @@ export const CustomAlert = ({
|
|||
]}
|
||||
onPress={() => handleActionPress(action)}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV && visible && isPrimary}
|
||||
>
|
||||
<Text style={[
|
||||
styles.actionText,
|
||||
|
|
@ -141,7 +145,7 @@ export const CustomAlert = ({
|
|||
]}>
|
||||
{action.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { memo, useCallback } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, FlatList } from 'react-native';
|
||||
import React, { memo, useCallback, useRef } from 'react';
|
||||
import { View, Text, StyleSheet, FlatList, Platform } from 'react-native';
|
||||
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
|
||||
|
||||
interface ProviderFilterProps {
|
||||
selectedProvider: string;
|
||||
|
|
@ -15,14 +16,24 @@ const ProviderFilter = memo(({
|
|||
theme
|
||||
}: ProviderFilterProps) => {
|
||||
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
||||
const listRef = useRef<FlatList<any> | null>(null);
|
||||
|
||||
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.filterChip,
|
||||
selectedProvider === item.id && styles.filterChipSelected
|
||||
]}
|
||||
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={[
|
||||
styles.filterChipText,
|
||||
|
|
@ -30,12 +41,13 @@ const ProviderFilter = memo(({
|
|||
]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
), [selectedProvider, onSelect, styles]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={providers}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
Clipboard,
|
||||
|
|
@ -16,6 +15,7 @@ import QualityBadge from './metadata/QualityBadge';
|
|||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useDownloads } from '../contexts/DownloadsContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
|
||||
|
||||
interface StreamCardProps {
|
||||
stream: Stream;
|
||||
|
|
@ -177,16 +177,22 @@ const StreamCard = memo(({
|
|||
|
||||
const isDebrid = streamInfo.isDebrid;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<View
|
||||
style={[
|
||||
styles.streamCard,
|
||||
isLoading && styles.streamCardLoading,
|
||||
isDebrid && styles.streamCardHighlighted
|
||||
isDebrid && styles.streamCardHighlighted,
|
||||
]}
|
||||
>
|
||||
<FocusableTouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
onPress={onPress}
|
||||
onLongPress={handleLongPress}
|
||||
disabled={isLoading}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
>
|
||||
{/* Scraper Logo */}
|
||||
{showLogos && scraperLogo && (
|
||||
|
|
@ -250,21 +256,23 @@ const StreamCard = memo(({
|
|||
</View>
|
||||
</View>
|
||||
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{settings?.enableDownloads !== false && (
|
||||
<TouchableOpacity
|
||||
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}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="icon"
|
||||
focusBorderRadius={15}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="download"
|
||||
size={20}
|
||||
color={theme.colors.highEmphasis}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<MaterialIcons name="download" size={20} color={theme.colors.highEmphasis} />
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
171
src/components/common/FocusablePressable.tsx
Normal file
171
src/components/common/FocusablePressable.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
|
||||
192
src/components/common/FocusableTouchableOpacity.tsx
Normal file
192
src/components/common/FocusableTouchableOpacity.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -3,13 +3,13 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
StatusBar,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Feather, MaterialIcons } from '@expo/vector-icons';
|
||||
import { FocusableTouchableOpacity } from './FocusableTouchableOpacity';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
|
|
@ -131,17 +131,20 @@ const ScreenHeader: React.FC<ScreenHeaderProps> = ({
|
|||
>
|
||||
<View style={styles.headerContent}>
|
||||
{showBackButton ? (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={onBackPress}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="icon"
|
||||
focusBorderRadius={999}
|
||||
>
|
||||
<IconComponent
|
||||
name={backIconName as any}
|
||||
size={24}
|
||||
color={currentTheme.colors.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
) : null}
|
||||
|
||||
{titleComponent ? (
|
||||
|
|
@ -164,17 +167,20 @@ const ScreenHeader: React.FC<ScreenHeaderProps> = ({
|
|||
{rightActionComponent ? (
|
||||
<View style={styles.rightActionContainer}>{rightActionComponent}</View>
|
||||
) : rightActionIcon && onRightActionPress ? (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.rightActionButton}
|
||||
onPress={onRightActionPress}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="icon"
|
||||
focusBorderRadius={999}
|
||||
>
|
||||
<IconComponent
|
||||
name={rightActionIcon as any}
|
||||
size={24}
|
||||
color={currentTheme.colors.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
) : (
|
||||
<View style={styles.rightActionPlaceholder} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import { useTraktContext } from '../../contexts/TraktContext';
|
|||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||
import { useWatchProgress } from '../../hooks/useWatchProgress';
|
||||
import { streamCacheService } from '../../services/streamCacheService';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
interface AppleTVHeroProps {
|
||||
featuredContent: StreamingContent | null;
|
||||
|
|
@ -1177,7 +1178,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
style={logoAnimatedStyle}
|
||||
>
|
||||
{currentItem.logo && !logoError[currentIndex] ? (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
activeOpacity={0.7}
|
||||
onPress={() => {
|
||||
if (currentItem) {
|
||||
|
|
@ -1188,6 +1189,10 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
}
|
||||
}}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={16}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.03}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
|
|
@ -1211,9 +1216,9 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
}}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
onPress={() => {
|
||||
if (currentItem) {
|
||||
|
|
@ -1224,13 +1229,17 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
}
|
||||
}}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={16}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.03}
|
||||
>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title} numberOfLines={2}>
|
||||
{currentItem.name}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
|
|
@ -1253,12 +1262,16 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
{/* Action Buttons - Play and Save buttons */}
|
||||
<View style={styles.buttonsContainer}>
|
||||
{/* Play Button */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.playButton]}
|
||||
onPress={handlePlayAction}
|
||||
activeOpacity={0.85}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={18}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.04}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={playButtonText === 'Resume' ? "replay" : "play-arrow"}
|
||||
|
|
@ -1266,21 +1279,25 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
color="#000"
|
||||
/>
|
||||
<Text style={styles.playButtonText}>{playButtonText}</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{/* Save Button */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.saveButton}
|
||||
onPress={handleSaveAction}
|
||||
activeOpacity={0.85}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={18}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.04}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={inLibrary ? "bookmark" : "bookmark-outline"}
|
||||
size={24}
|
||||
color="white"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Pagination Dots */}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { storageService } from '../../services/storageService';
|
|||
import { TraktService } from '../../services/traktService';
|
||||
import { useTraktContext } from '../../contexts/TraktContext';
|
||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
interface ContentItemProps {
|
||||
item: StreamingContent;
|
||||
|
|
@ -302,12 +303,18 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
return (
|
||||
<>
|
||||
<Animated.View style={[styles.itemContainer, { width: finalWidth }]} entering={FadeIn.duration(300)}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.contentItem, { width: finalWidth, aspectRatio: finalAspectRatio, borderRadius }]}
|
||||
activeOpacity={0.7}
|
||||
onPress={handlePress}
|
||||
onLongPress={handleLongPress}
|
||||
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 }]}>
|
||||
{/* Image with FastImage for aggressive caching */}
|
||||
|
|
@ -362,7 +369,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{settings.showPosterTitles && (
|
||||
<Text
|
||||
style={[
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { stremioService } from '../../services/stremioService';
|
|||
import { streamCacheService } from '../../services/streamCacheService';
|
||||
import { useSettings } from '../../hooks/useSettings';
|
||||
import CustomAlert from '../../components/CustomAlert';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
// Define interface for continue watching items
|
||||
interface ContinueWatchingItem extends StreamingContent {
|
||||
|
|
@ -1081,7 +1082,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
|
||||
// Memoized render function for continue watching items
|
||||
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.wideContentItem,
|
||||
{
|
||||
|
|
@ -1096,6 +1097,11 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
onPress={() => handleContentPress(item)}
|
||||
onLongPress={() => handleLongPress(item)}
|
||||
delayLongPress={800}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={14}
|
||||
focusRingColor={currentTheme.colors.primary}
|
||||
focusRingWidth={3}
|
||||
focusScale={isTV ? 1.06 : 1.04}
|
||||
>
|
||||
{/* Poster Image */}
|
||||
<View style={[
|
||||
|
|
@ -1242,7 +1248,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet]);
|
||||
|
||||
// Memoized key extractor
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
StyleSheet,
|
||||
Modal,
|
||||
Pressable,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
Dimensions,
|
||||
Platform
|
||||
|
|
@ -28,6 +27,7 @@ import {
|
|||
GestureHandlerRootView,
|
||||
} from 'react-native-gesture-handler';
|
||||
import { StreamingContent } from '../../services/catalogService';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
interface DropUpMenuProps {
|
||||
visible: boolean;
|
||||
|
|
@ -184,7 +184,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
</View>
|
||||
<View style={styles.menuOptions}>
|
||||
{menuOptions.map((option, index) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={option.action}
|
||||
style={[
|
||||
styles.menuOption,
|
||||
|
|
@ -195,6 +195,10 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
onOptionSelect(option.action);
|
||||
onClose();
|
||||
}}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={16}
|
||||
hasTVPreferredFocus={Platform.isTV && visible && index === 0}
|
||||
>
|
||||
<MaterialIcons
|
||||
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}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { SkeletonFeatured } from './SkeletonLoaders';
|
|||
import { hasValidLogoFormat, isTmdbUrl } from '../../utils/logoUtils';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
interface FeaturedContentProps {
|
||||
featuredContent: StreamingContent | null;
|
||||
|
|
@ -495,7 +496,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
)}
|
||||
|
||||
<Animated.View style={[styles.tabletButtons as ViewStyle, buttonsAnimatedStyle]}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.tabletPlayButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
||||
onPress={() => {
|
||||
if (featuredContent) {
|
||||
|
|
@ -507,12 +508,17 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
}}
|
||||
activeOpacity={0.8}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={30}
|
||||
focusRingColor={currentTheme.colors.primary}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.04}
|
||||
>
|
||||
<MaterialIcons name="play-arrow" size={28} color={currentTheme.colors.black} />
|
||||
<Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||
Play Now
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
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>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
||||
onPress={() => {
|
||||
if (featuredContent) {
|
||||
|
|
@ -643,12 +649,17 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
}}
|
||||
activeOpacity={0.8}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={30}
|
||||
focusRingColor={currentTheme.colors.primary}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.04}
|
||||
>
|
||||
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
||||
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||
Play
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.infoButton as ViewStyle}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import { TMDBService } from '../../services/tmdbService';
|
|||
import TrailerService from '../../services/trailerService';
|
||||
import TrailerPlayer from '../video/TrailerPlayer';
|
||||
import { HERO_HEIGHT, SCREEN_WIDTH as width, IS_TABLET as isTablet } from '../../constants/dimensions';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
const { height } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -344,7 +345,7 @@ const ActionButtons = memo(({
|
|||
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
||||
{/* Single Row Layout - Play, Save, and optionally Collection/Ratings */}
|
||||
<View style={styles.singleRowLayout}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
playButtonStyle,
|
||||
isTablet && styles.tabletPlayButton,
|
||||
|
|
@ -354,6 +355,10 @@ const ActionButtons = memo(({
|
|||
activeOpacity={0.85}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTablet ? 20 : 18}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.04}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={(() => {
|
||||
|
|
@ -366,9 +371,9 @@ const ActionButtons = memo(({
|
|||
color={isWatched && type === 'movie' ? "#fff" : "#000"}
|
||||
/>
|
||||
<Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
styles.infoButton,
|
||||
|
|
@ -378,6 +383,10 @@ const ActionButtons = memo(({
|
|||
onPress={handleSaveAction}
|
||||
activeOpacity={0.85}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTablet ? 20 : 18}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.04}
|
||||
>
|
||||
{Platform.OS === 'ios' ? (
|
||||
GlassViewComp && liquidGlassAvailable ? (
|
||||
|
|
@ -399,15 +408,19 @@ const ActionButtons = memo(({
|
|||
<Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}>
|
||||
{inLibrary ? 'Saved' : 'Save'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{/* Trakt Collection Button */}
|
||||
{hasTraktCollection && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
||||
onPress={handleCollectionAction}
|
||||
activeOpacity={0.85}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={999}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.06}
|
||||
>
|
||||
{Platform.OS === 'ios' ? (
|
||||
GlassViewComp && liquidGlassAvailable ? (
|
||||
|
|
@ -426,16 +439,20 @@ const ActionButtons = memo(({
|
|||
size={isTablet ? 28 : 24}
|
||||
color={isInCollection ? "#3498DB" : currentTheme.colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Ratings Button (for series) */}
|
||||
{hasRatings && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
||||
onPress={handleRatingsPress}
|
||||
activeOpacity={0.85}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={999}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.06}
|
||||
>
|
||||
{Platform.OS === 'ios' ? (
|
||||
GlassViewComp && liquidGlassAvailable ? (
|
||||
|
|
@ -454,7 +471,7 @@ const ActionButtons = memo(({
|
|||
size={isTablet ? 28 : 24}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
|
@ -1757,7 +1774,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
right: width >= 768 ? 32 : 16,
|
||||
zIndex: 1000,
|
||||
}}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
// Extract episode info if it's a series
|
||||
let episodeData = null;
|
||||
|
|
@ -1782,6 +1799,10 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
}}
|
||||
activeOpacity={0.7}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={20}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.06}
|
||||
style={{
|
||||
padding: 8,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
|
|
@ -1793,19 +1814,27 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
|||
size={24}
|
||||
color="white"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<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
|
||||
name="arrow-back"
|
||||
size={28}
|
||||
color="#fff"
|
||||
style={styles.backButtonIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
{/* Ultra-light Gradient with subtle dynamic background blend */}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useTheme } from '../../contexts/ThemeContext';
|
|||
import { TMDBService } from '../../services/tmdbService';
|
||||
import { catalogService } from '../../services/catalogService';
|
||||
import CustomAlert from '../../components/CustomAlert';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -117,9 +118,15 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
|||
};
|
||||
|
||||
const renderItem = ({ item }: { item: StreamingContent }) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.itemContainer, { width: posterWidth, marginRight: itemSpacing }]}
|
||||
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
|
||||
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}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
|
||||
if (loadingRecommendations) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { TraktService } from '../../services/traktService';
|
|||
import { watchedService } from '../../services/watchedService';
|
||||
import { logger } from '../../utils/logger';
|
||||
import { mmkvStorage } from '../../services/mmkvStorage';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
// Enhanced responsive breakpoints for Seasons Section
|
||||
const BREAKPOINTS = {
|
||||
|
|
@ -779,7 +780,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
]}>Seasons</Text>
|
||||
|
||||
{/* Dropdown Toggle Button */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.seasonViewToggle,
|
||||
{
|
||||
|
|
@ -796,6 +797,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
]}
|
||||
activeOpacity={0.7}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTV ? 10 : isLargeTablet ? 8 : isTablet ? 6 : 6}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.03}
|
||||
>
|
||||
<Text style={[
|
||||
styles.seasonViewToggleText,
|
||||
|
|
@ -808,7 +813,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
]}>
|
||||
{seasonViewMode === 'posters' ? 'Posters' : 'Text'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
|
|
@ -846,7 +851,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
key={season}
|
||||
style={{ opacity: textViewVisible ? 1 : 0 }}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.seasonTextButton,
|
||||
{
|
||||
|
|
@ -860,6 +865,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
]}
|
||||
onPress={() => onSeasonChange(season)}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.03}
|
||||
>
|
||||
<Text style={[
|
||||
styles.seasonTextButtonText,
|
||||
|
|
@ -873,7 +882,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
]} numberOfLines={1}>
|
||||
{season === 0 ? 'Specials' : `Season ${season}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -885,7 +894,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
key={season}
|
||||
style={{ opacity: posterViewVisible ? 1 : 0 }}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.seasonButton,
|
||||
{
|
||||
|
|
@ -896,6 +905,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
]}
|
||||
onPress={() => onSeasonChange(season)}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.03}
|
||||
>
|
||||
<View style={[
|
||||
styles.seasonPosterContainer,
|
||||
|
|
@ -937,7 +950,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
>
|
||||
{season === 0 ? 'Specials' : `Season ${season}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}}
|
||||
|
|
@ -1022,7 +1035,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
const showProgress = progress && progressPercent < 85;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={episode.id}
|
||||
style={[
|
||||
styles.episodeCardVertical,
|
||||
|
|
@ -1038,6 +1051,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
|||
delayLongPress={400}
|
||||
activeOpacity={0.7}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.02}
|
||||
>
|
||||
<View style={[
|
||||
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')}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { logger } from '../../utils/logger';
|
|||
import TrailerService from '../../services/trailerService';
|
||||
import TrailerModal from './TrailerModal';
|
||||
import Animated, { useSharedValue, withTiming, withDelay, useAnimatedStyle } from 'react-native-reanimated';
|
||||
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||
|
||||
// Enhanced responsive breakpoints for Trailers Section
|
||||
const BREAKPOINTS = {
|
||||
|
|
@ -517,7 +518,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
|
||||
{/* Category Selector - Right Aligned */}
|
||||
{trailerCategories.length > 0 && selectedCategory && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.categorySelector,
|
||||
{
|
||||
|
|
@ -531,6 +532,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
onPress={toggleDropdown}
|
||||
activeOpacity={0.8}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.03}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
|
|
@ -551,7 +556,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
size={isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18}
|
||||
color="rgba(255,255,255,0.7)"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
|
@ -575,7 +580,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
||||
}]}>
|
||||
{trailerCategories.map(category => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={category}
|
||||
style={[
|
||||
styles.dropdownItem,
|
||||
|
|
@ -587,6 +592,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
onPress={() => handleCategorySelect(category)}
|
||||
activeOpacity={0.7}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTV ? 18 : isLargeTablet ? 16 : isTablet ? 14 : 14}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.02}
|
||||
>
|
||||
<View style={styles.dropdownItemContent}>
|
||||
<View style={[
|
||||
|
|
@ -626,7 +635,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
{trailers[category].length}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
|
@ -656,7 +665,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
{ width: trailerCardWidth }
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.trailerCard,
|
||||
{
|
||||
|
|
@ -667,6 +676,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
onPress={() => handleTrailerPress(trailer)}
|
||||
activeOpacity={0.9}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
|
||||
focusRingWidth={3}
|
||||
focusScale={1.03}
|
||||
>
|
||||
{/* Thumbnail with Gradient Overlay */}
|
||||
<View style={styles.thumbnailWrapper}>
|
||||
|
|
@ -688,7 +701,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
}
|
||||
]} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{/* Trailer Info Below Card */}
|
||||
<View style={styles.trailerInfoBelow}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native';
|
||||
import { View, Text, Pressable, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
|
|
@ -9,6 +9,7 @@ import Animated, {
|
|||
} from 'react-native-reanimated';
|
||||
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
interface AudioTrackModalProps {
|
||||
showAudioModal: boolean;
|
||||
|
|
@ -38,17 +39,17 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
|||
return (
|
||||
<View style={StyleSheet.absoluteFill} zIndex={9999}>
|
||||
{/* Backdrop matching SubtitleModal */}
|
||||
<TouchableOpacity
|
||||
<Pressable
|
||||
style={StyleSheet.absoluteFill}
|
||||
activeOpacity={1}
|
||||
onPress={handleClose}
|
||||
focusable={false}
|
||||
>
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
|
||||
{/* Center Alignment Container */}
|
||||
<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;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={track.id}
|
||||
onPress={() => {
|
||||
selectAudioTrack(track.id);
|
||||
|
|
@ -93,6 +94,10 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
|||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV && isSelected}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{
|
||||
|
|
@ -104,7 +109,7 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
|||
</Text>
|
||||
</View>
|
||||
{isSelected && <MaterialIcons name="check" size={18} color="black" />}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
})}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native';
|
||||
import { View, Text, Pressable, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
|
|
@ -11,6 +11,7 @@ import { Episode } from '../../../types/metadata';
|
|||
import { Stream } from '../../../types/streams';
|
||||
import { stremioService } from '../../../services/stremioService';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
interface EpisodeStreamsModalProps {
|
||||
visible: boolean;
|
||||
|
|
@ -142,17 +143,17 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
|
|||
return (
|
||||
<View style={StyleSheet.absoluteFill} zIndex={10000}>
|
||||
{/* Backdrop */}
|
||||
<TouchableOpacity
|
||||
<Pressable
|
||||
style={StyleSheet.absoluteFill}
|
||||
activeOpacity={1}
|
||||
onPress={onClose}
|
||||
focusable={false}
|
||||
>
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
|
||||
<Animated.View
|
||||
entering={SlideInRight.duration(300)}
|
||||
|
|
@ -218,7 +219,7 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
|
|||
const quality = getQualityFromTitle(stream.title) || stream.quality;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={`${providerId}-${index}`}
|
||||
style={{
|
||||
padding: 8,
|
||||
|
|
@ -232,6 +233,10 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
|
|||
onClose();
|
||||
}}
|
||||
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={{ flex: 1 }}>
|
||||
|
|
@ -248,7 +253,7 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
|
|||
)}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform, ActivityIndicator } from 'react-native';
|
||||
import { View, Text, Pressable, ScrollView, useWindowDimensions, StyleSheet, Platform, ActivityIndicator } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
|
|
@ -12,6 +12,7 @@ import { EpisodeCard } from '../cards/EpisodeCard';
|
|||
import { storageService } from '../../../services/storageService';
|
||||
import { TraktService } from '../../../services/traktService';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
interface EpisodesModalProps {
|
||||
showEpisodesModal: boolean;
|
||||
|
|
@ -97,9 +98,9 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
|
|||
|
||||
return (
|
||||
<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)' }} />
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
|
||||
<Animated.View
|
||||
entering={SlideInRight.duration(300)}
|
||||
|
|
@ -127,7 +128,7 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
|
|||
if (b === 0) return -1;
|
||||
return a - b;
|
||||
}).map((season) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={season}
|
||||
onPress={() => setSelectedSeason(season)}
|
||||
style={{
|
||||
|
|
@ -138,6 +139,10 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
|
|||
borderWidth: 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={{
|
||||
color: selectedSeason === season ? 'black' : 'white',
|
||||
|
|
@ -145,7 +150,7 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
|
|||
}}>
|
||||
{season === 0 ? 'Specials' : `Season ${season}`}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, useWindowDimensions, Platform } from 'react-native';
|
||||
import { View, Text, Pressable, StyleSheet, useWindowDimensions, Platform } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
|
|
@ -7,6 +7,7 @@ import Animated, {
|
|||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from 'react-native-reanimated';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
// Check if running on TV platform
|
||||
const isTV = Platform.isTV;
|
||||
|
|
@ -58,9 +59,9 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
|||
|
||||
return (
|
||||
<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)' }} />
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(300)}
|
||||
|
|
@ -111,15 +112,20 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
|||
{errorDetails || 'An unknown error occurred during playback.'}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
{!!ExpoClipboard && (
|
||||
<FocusableTouchableOpacity
|
||||
onPress={handleCopy}
|
||||
activeOpacity={0.9}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={12}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 8,
|
||||
marginBottom: 24,
|
||||
opacity: 0.8
|
||||
opacity: 0.9
|
||||
}}
|
||||
>
|
||||
<MaterialIcons
|
||||
|
|
@ -131,9 +137,10 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
|||
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontWeight: '500' }}>
|
||||
{copied ? 'Copied to clipboard' : 'Copy error details'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
paddingVertical: 12,
|
||||
|
|
@ -144,6 +151,10 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
|||
}}
|
||||
onPress={handleClose}
|
||||
activeOpacity={0.9}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<Text style={{
|
||||
color: 'black',
|
||||
|
|
@ -152,7 +163,7 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
|||
}}>
|
||||
Dismiss
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { View, TouchableOpacity, Animated, ActivityIndicator, StyleSheet, Image } from 'react-native';
|
||||
import { View, Animated, ActivityIndicator, StyleSheet, Image, Platform } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import Reanimated, {
|
||||
|
|
@ -12,6 +12,7 @@ import Reanimated, {
|
|||
withDelay
|
||||
} from 'react-native-reanimated';
|
||||
import { styles } from '../utils/playerStyles';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
visible: boolean;
|
||||
|
|
@ -118,13 +119,17 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({
|
|||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.loadingCloseButton}
|
||||
onPress={onClose}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="icon"
|
||||
focusBorderRadius={999}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="close" size={24} color="#ffffff" />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.openingContent}>
|
||||
{hasLogo && logo ? (
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { View, Text, TouchableOpacity } from 'react-native';
|
||||
import { View, Text, Platform } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { styles } from '../utils/playerStyles';
|
||||
import { formatTime } from '../utils/playerUtils';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
interface ResumeOverlayProps {
|
||||
showResumeOverlay: boolean;
|
||||
|
|
@ -71,20 +72,27 @@ export const ResumeOverlay: React.FC<ResumeOverlayProps> = ({
|
|||
</View>
|
||||
|
||||
<View style={styles.resumeButtons}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.resumeButton}
|
||||
onPress={handleStartFromBeginning}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={12}
|
||||
>
|
||||
<Ionicons name="refresh" size={16} color="white" style={styles.buttonIcon} />
|
||||
<Text style={styles.resumeButtonText}>Start Over</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.resumeButton, styles.resumeFromButton]}
|
||||
onPress={handleResume}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<Ionicons name="play" size={16} color="white" style={styles.buttonIcon} />
|
||||
<Text style={styles.resumeButtonText}>Resume</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Animated, {
|
|||
SlideOutRight,
|
||||
} from 'react-native-reanimated';
|
||||
import { Stream } from '../../../types/streams';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
interface SourcesModalProps {
|
||||
showSourcesModal: boolean;
|
||||
|
|
@ -168,7 +169,7 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
|
|||
const quality = getQualityFromTitle(stream.title) || stream.quality;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={`${providerId}-${index}`}
|
||||
style={{
|
||||
padding: 8,
|
||||
|
|
@ -181,6 +182,10 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
|
|||
onPress={() => handleStreamSelect(stream)}
|
||||
activeOpacity={0.7}
|
||||
disabled={isChangingSource === true}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV && (isSelected || (providerId === sortedProviders[0]?.[0] && index === 0))}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
|
|
@ -227,7 +232,7 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
|
|||
)}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, useWindowDimensions, StyleSheet } from 'react-native';
|
||||
import { View, Text, Pressable, useWindowDimensions, StyleSheet, Platform } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
|
|
@ -9,6 +9,7 @@ import Animated, {
|
|||
useAnimatedStyle,
|
||||
withTiming,
|
||||
} from 'react-native-reanimated';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
interface SpeedModalProps {
|
||||
showSpeedModal: boolean;
|
||||
|
|
@ -31,7 +32,15 @@ const MorphingButton = ({ label, isSelected, onPress, isSmall = false }: any) =>
|
|||
});
|
||||
|
||||
return (
|
||||
<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]}>
|
||||
<Text style={{
|
||||
color: isSelected && !isSmall ? 'black' : 'white',
|
||||
|
|
@ -41,7 +50,7 @@ const MorphingButton = ({ label, isSelected, onPress, isSmall = false }: any) =>
|
|||
{label}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -62,14 +71,14 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
|
|||
if (!showSpeedModal) return null;
|
||||
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFill} zIndex={9999}>
|
||||
<TouchableOpacity
|
||||
<View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
|
||||
<Pressable
|
||||
style={StyleSheet.absoluteFill}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowSpeedModal(false)}
|
||||
focusable={false}
|
||||
>
|
||||
<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 }}>
|
||||
<Animated.View
|
||||
|
|
@ -104,9 +113,13 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
|
|||
|
||||
{/* On Hold Section */}
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => setHoldToSpeedEnabled(!holdToSpeedEnabled)}
|
||||
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>
|
||||
<View style={{
|
||||
|
|
@ -116,7 +129,7 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
|
|||
}}>
|
||||
<View style={{ width: 14, height: 14, borderRadius: 7, backgroundColor: holdToSpeedEnabled ? 'black' : 'white' }} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{holdToSpeedEnabled && (
|
||||
<Animated.View entering={FadeIn} style={{ flexDirection: 'row', gap: 8 }}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, Platform, useWindowDimensions, StyleSheet } from 'react-native';
|
||||
import { View, Text, Pressable, ScrollView, Platform, useWindowDimensions, StyleSheet } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
|
|
@ -11,6 +11,7 @@ import Animated, {
|
|||
} from 'react-native-reanimated';
|
||||
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
|
||||
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
|
||||
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||
|
||||
interface SubtitleModalsProps {
|
||||
showSubtitleModal: boolean;
|
||||
|
|
@ -65,13 +66,21 @@ const MorphingTab = ({ label, isSelected, onPress }: any) => {
|
|||
}));
|
||||
|
||||
return (
|
||||
<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]}>
|
||||
<Text style={{ color: isSelected ? 'black' : 'white', fontWeight: isSelected ? '700' : '400', fontSize: 13 }}>
|
||||
{label}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -112,11 +121,11 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
if (!showSubtitleModal) return null;
|
||||
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFill} zIndex={9999}>
|
||||
<View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
|
||||
{/* 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)' }} />
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
|
||||
{/* Centered Modal Container */}
|
||||
<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 }}>
|
||||
{activeTab === 'built-in' && (
|
||||
<View style={{ gap: 8 }}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }}
|
||||
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV && selectedTextTrack === -1}
|
||||
>
|
||||
<Text style={{ color: selectedTextTrack === -1 ? 'black' : 'rgba(96, 20, 16)', fontWeight: '600' }}>None</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{ksTextTracks.map((track) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={track.id}
|
||||
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' }}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV && selectedTextTrack === track.id}
|
||||
>
|
||||
<Text style={{ color: selectedTextTrack === track.id ? 'black' : 'white' }}>{getTrackDisplayName(track)}</Text>
|
||||
{selectedTextTrack === track.id && <MaterialIcons name="check" size={18} color="black" />}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -171,23 +188,34 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
{activeTab === 'addon' && (
|
||||
<View style={{ gap: 8 }}>
|
||||
{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" />
|
||||
<Text style={{ color: 'white', marginTop: 10 }}>Search Online Subtitles</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
) : (
|
||||
availableSubtitles.map((sub) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={sub.id}
|
||||
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>
|
||||
<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>
|
||||
</View>
|
||||
{selectedOnlineSubtitleId === sub.id && <MaterialIcons name="check" size={18} color="black" />}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))
|
||||
)}
|
||||
</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>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
|
||||
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
|
||||
|
|
@ -241,33 +269,45 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
setSubtitleLineHeightMultiplier(1.2);
|
||||
}}
|
||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={20}
|
||||
>
|
||||
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 11 : 12 }}>Default</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false);
|
||||
}}
|
||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={20}
|
||||
>
|
||||
<Text style={{ color: '#FFD700', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Yellow</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5);
|
||||
}}
|
||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={20}
|
||||
>
|
||||
<Text style={{ color: '#22C55E', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>High Contrast</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3);
|
||||
}}
|
||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={20}
|
||||
>
|
||||
<Text style={{ color: '#3B82F6', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Large</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -283,15 +323,27 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Font Size</Text>
|
||||
</View>
|
||||
<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" />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<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>
|
||||
</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" />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<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)" />
|
||||
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Show Background</Text>
|
||||
</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 }}
|
||||
onPress={toggleSubtitleBackground}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={15}
|
||||
>
|
||||
<View style={{ width: 24, height: 24, backgroundColor: subtitleBackground ? 'black' : 'white', borderRadius: 12 }} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -321,7 +376,14 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
</View>
|
||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
|
||||
{['#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>
|
||||
|
|
@ -329,95 +391,175 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
<Text style={{ color: 'white', fontWeight: '600' }}>Align</Text>
|
||||
<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 => (
|
||||
<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" />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text style={{ color: 'white', fontWeight: '600' }}>Bottom Offset</Text>
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<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>
|
||||
</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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text>
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<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>
|
||||
</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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<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>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text style={{ color: 'white' }}>Outline Color</Text>
|
||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||
{['#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 style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text style={{ color: 'white' }}>Outline Width</Text>
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<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>
|
||||
</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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flexDirection: isCompact ? 'column' : 'row', justifyContent: 'space-between', gap: 12 }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ color: 'white', fontWeight: '600' }}>Letter Spacing</Text>
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<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>
|
||||
</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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{ color: 'white', fontWeight: '600' }}>Line Height</Text>
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<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>
|
||||
</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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -425,21 +567,33 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text>
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<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>
|
||||
</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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</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>
|
||||
</View>
|
||||
<View style={{ alignItems: 'flex-end', marginTop: 8 }}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
|
||||
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
|
||||
|
|
@ -447,9 +601,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
|||
setSubtitleLineHeightMultiplier(1.2); setSubtitleOffsetSec(0);
|
||||
}}
|
||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 8, backgroundColor: 'rgba(255,255,255,0.1)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={8}
|
||||
>
|
||||
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 12 : 14 }}>Reset to defaults</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
Text,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
KeyboardAvoidingView,
|
||||
|
|
@ -58,6 +57,7 @@ import Animated, {
|
|||
Extrapolate,
|
||||
runOnJS
|
||||
} from 'react-native-reanimated';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
|
@ -302,7 +302,7 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
|
|||
const { currentTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.suggestionChip, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||
onPress={onPress}
|
||||
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}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress);
|
||||
|
||||
|
|
@ -684,7 +684,7 @@ const AIChatScreen: React.FC = () => {
|
|||
headerAnimatedStyle
|
||||
]}>
|
||||
<View style={styles.headerContent}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
if (Platform.OS === 'android') {
|
||||
modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => {
|
||||
|
|
@ -697,7 +697,7 @@ const AIChatScreen: React.FC = () => {
|
|||
style={styles.backButton}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.headerInfo}>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
|
|
@ -821,7 +821,7 @@ const AIChatScreen: React.FC = () => {
|
|||
blurOnSubmit={false}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.sendButton,
|
||||
{
|
||||
|
|
@ -837,7 +837,7 @@ const AIChatScreen: React.FC = () => {
|
|||
size={20}
|
||||
color={inputText.trim() ? 'white' : currentTheme.colors.mediumEmphasis}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native';
|
||||
import { View, Text, StyleSheet, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
|
@ -9,6 +9,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
|||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import * as Haptics from 'expo-haptics';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const AccountManageScreen: React.FC = () => {
|
||||
const navigation = useNavigation();
|
||||
|
|
@ -97,9 +98,9 @@ const AccountManageScreen: React.FC = () => {
|
|||
colors={[currentTheme.colors.darkBackground, '#111318']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Account</Text>
|
||||
<View style={{ width: 22, height: 22 }} />
|
||||
</Animated.View>
|
||||
|
|
@ -185,7 +186,7 @@ const AccountManageScreen: React.FC = () => {
|
|||
</View>
|
||||
|
||||
{/* Save and Sign out */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
activeOpacity={0.85}
|
||||
style={[styles.saveButton, { backgroundColor: currentTheme.colors.elevation2, borderColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={handleSave}
|
||||
|
|
@ -199,9 +200,9 @@ const AccountManageScreen: React.FC = () => {
|
|||
<Text style={styles.saveText}>Save changes</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
activeOpacity={0.85}
|
||||
style={[
|
||||
styles.signOutButton,
|
||||
|
|
@ -211,7 +212,7 @@ const AccountManageScreen: React.FC = () => {
|
|||
>
|
||||
<MaterialIcons name="logout" size={18} color="#fff" style={{ marginRight: 8 }} />
|
||||
<Text style={styles.signOutText}>Sign out</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</Animated.View>
|
||||
<CustomAlert
|
||||
visible={alertVisible}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
StyleSheet,
|
||||
FlatList,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
|
|
@ -48,6 +47,7 @@ if (Platform.OS === 'ios') {
|
|||
// Removed community blur and expo-constants for Android overlay
|
||||
import axios from 'axios';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
// Extend Manifest type to include logo only (remove disabled status)
|
||||
interface ExtendedManifest extends Manifest {
|
||||
|
|
@ -974,7 +974,7 @@ const AddonsScreen = () => {
|
|||
<View style={styles.addonItem}>
|
||||
{reorderMode && (
|
||||
<View style={styles.reorderButtons}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.reorderButton, isFirstItem && styles.disabledButton]}
|
||||
onPress={() => moveAddonUp(item)}
|
||||
disabled={isFirstItem}
|
||||
|
|
@ -984,8 +984,8 @@ const AddonsScreen = () => {
|
|||
size={20}
|
||||
color={isFirstItem ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.reorderButton, isLastItem && styles.disabledButton]}
|
||||
onPress={() => moveAddonDown(item)}
|
||||
disabled={isLastItem}
|
||||
|
|
@ -995,7 +995,7 @@ const AddonsScreen = () => {
|
|||
size={20}
|
||||
color={isLastItem ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
@ -1030,20 +1030,20 @@ const AddonsScreen = () => {
|
|||
{!reorderMode ? (
|
||||
<>
|
||||
{isConfigurable && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item, item.transport)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
{!stremioService.isPreInstalledAddon(item.id) && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleRemoveAddon(item)}
|
||||
>
|
||||
<MaterialIcons name="delete" size={20} color={colors.error} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -1098,14 +1098,14 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
<View style={styles.addonActionButtons}>
|
||||
{isConfigurable && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(manifest, transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(transportUrl)}
|
||||
disabled={installing}
|
||||
|
|
@ -1115,7 +1115,7 @@ const AddonsScreen = () => {
|
|||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -1134,17 +1134,17 @@ const AddonsScreen = () => {
|
|||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Reorder Mode Toggle Button */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.headerButton, reorderMode && styles.activeHeaderButton]}
|
||||
onPress={toggleReorderMode}
|
||||
>
|
||||
|
|
@ -1153,10 +1153,10 @@ const AddonsScreen = () => {
|
|||
size={24}
|
||||
color={reorderMode ? colors.primary : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{/* Refresh Button */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={refreshAddons}
|
||||
disabled={loading}
|
||||
|
|
@ -1166,7 +1166,7 @@ const AddonsScreen = () => {
|
|||
size={24}
|
||||
color={loading ? colors.mediumGray : colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -1221,7 +1221,7 @@ const AddonsScreen = () => {
|
|||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.addButton, { opacity: installing || !addonUrl ? 0.6 : 1 }]}
|
||||
onPress={() => handleAddAddon()}
|
||||
disabled={installing || !addonUrl}
|
||||
|
|
@ -1229,7 +1229,7 @@ const AddonsScreen = () => {
|
|||
<Text style={styles.addButtonText}>
|
||||
{installing ? 'Loading...' : 'Add Addon'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -1289,14 +1289,14 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{promoAddon.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.installButton}
|
||||
onPress={() => handleAddAddon(PROMO_ADDON_URL)}
|
||||
disabled={installing}
|
||||
|
|
@ -1306,7 +1306,7 @@ const AddonsScreen = () => {
|
|||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.addonDescription}>
|
||||
|
|
@ -1371,14 +1371,14 @@ const AddonsScreen = () => {
|
|||
</View>
|
||||
<View style={styles.addonActions}>
|
||||
{item.manifest.behaviorHints?.configurable && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.configButton}
|
||||
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
|
||||
>
|
||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||
onPress={() => handleAddAddon(item.transportUrl)}
|
||||
disabled={installing}
|
||||
|
|
@ -1388,7 +1388,7 @@ const AddonsScreen = () => {
|
|||
) : (
|
||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -1435,14 +1435,14 @@ const AddonsScreen = () => {
|
|||
<>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Install Addon</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
setShowConfirmModal(false);
|
||||
setAddonDetails(null);
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="close" size={24} color={colors.white} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
|
|
@ -1504,7 +1504,7 @@ const AddonsScreen = () => {
|
|||
</ScrollView>
|
||||
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.modalButton, styles.cancelButton]}
|
||||
onPress={() => {
|
||||
setShowConfirmModal(false);
|
||||
|
|
@ -1512,8 +1512,8 @@ const AddonsScreen = () => {
|
|||
}}
|
||||
>
|
||||
<Text style={styles.modalButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.modalButton, styles.installButton]}
|
||||
onPress={confirmInstallAddon}
|
||||
disabled={installing}
|
||||
|
|
@ -1523,7 +1523,7 @@ const AddonsScreen = () => {
|
|||
) : (
|
||||
<Text style={styles.modalButtonText}>Install</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Dimensions, Animated, Easing, Keyboard } from 'react-native';
|
||||
import { View, TextInput, Text, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Dimensions, Animated, Easing, Keyboard } from 'react-native';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
|
@ -9,6 +9,7 @@ import { useNavigation, useRoute } from '@react-navigation/native';
|
|||
import * as Haptics from 'expo-haptics';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -277,9 +278,9 @@ const AuthScreen: React.FC = () => {
|
|||
]}
|
||||
>
|
||||
{navigation.canGoBack() && (
|
||||
<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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
<Animated.Text style={[styles.heading, { color: currentTheme.colors.white, opacity: titleOpacity, transform: [{ translateY: titleTranslateY }] }]}>
|
||||
{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)' }]}
|
||||
onPress={toggleWarningDetails}
|
||||
activeOpacity={0.8}
|
||||
|
|
@ -316,7 +317,7 @@ const AuthScreen: React.FC = () => {
|
|||
Read more {showWarningDetails ? '▼' : '▶'}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{/* Expanded Details */}
|
||||
{showWarningDetails && (
|
||||
|
|
@ -392,7 +393,7 @@ const AuthScreen: React.FC = () => {
|
|||
},
|
||||
]}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.switchButton,
|
||||
]}
|
||||
|
|
@ -402,8 +403,8 @@ const AuthScreen: React.FC = () => {
|
|||
<Text style={[styles.switchText, { color: mode === 'signin' ? '#fff' : currentTheme.colors.textMuted }]}>
|
||||
Sign In
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.switchButton,
|
||||
signupDisabled && styles.disabledButton,
|
||||
|
|
@ -420,7 +421,7 @@ const AuthScreen: React.FC = () => {
|
|||
]}>
|
||||
Sign Up {signupDisabled && '(Disabled)'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Email Input */}
|
||||
|
|
@ -477,13 +478,13 @@ const AuthScreen: React.FC = () => {
|
|||
returnKeyType="done"
|
||||
onSubmitEditing={handleSubmit}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowPassword(p => !p)} style={styles.eyeButton}>
|
||||
<FocusableTouchableOpacity onPress={() => setShowPassword(p => !p)} style={styles.eyeButton}>
|
||||
<MaterialIcons
|
||||
name={showPassword ? 'visibility-off' : 'visibility'}
|
||||
size={16}
|
||||
color={currentTheme.colors.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{Platform.OS !== 'android' && isPasswordValid && (
|
||||
<MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} />
|
||||
)}
|
||||
|
|
@ -515,13 +516,13 @@ const AuthScreen: React.FC = () => {
|
|||
returnKeyType="done"
|
||||
onSubmitEditing={handleSubmit}
|
||||
/>
|
||||
<TouchableOpacity onPress={() => setShowConfirm(p => !p)} style={styles.eyeButton}>
|
||||
<FocusableTouchableOpacity onPress={() => setShowConfirm(p => !p)} style={styles.eyeButton}>
|
||||
<MaterialIcons
|
||||
name={showConfirm ? 'visibility-off' : 'visibility'}
|
||||
size={16}
|
||||
color={currentTheme.colors.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{Platform.OS !== 'android' && passwordsMatch && isConfirmValid && (
|
||||
<MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} />
|
||||
)}
|
||||
|
|
@ -539,7 +540,7 @@ const AuthScreen: React.FC = () => {
|
|||
|
||||
{/* Submit Button */}
|
||||
<Animated.View style={{ transform: [{ scale: ctaScale }] }}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.ctaButton,
|
||||
{
|
||||
|
|
@ -579,12 +580,12 @@ const AuthScreen: React.FC = () => {
|
|||
{mode === 'signin' ? 'Sign In' : 'Create Account'}
|
||||
</Animated.Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
{/* Switch Mode */}
|
||||
{!signupDisabled && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
|
||||
activeOpacity={0.7}
|
||||
style={{ marginTop: 16 }}
|
||||
|
|
@ -595,7 +596,7 @@ const AuthScreen: React.FC = () => {
|
|||
{mode === 'signin' ? 'Sign up' : 'Sign in'}
|
||||
</Text>
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Signup disabled message */}
|
||||
|
|
@ -608,7 +609,7 @@ const AuthScreen: React.FC = () => {
|
|||
)}
|
||||
|
||||
{/* Skip sign in - more prominent when coming from onboarding */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={handleSkipAuth}
|
||||
activeOpacity={0.85}
|
||||
style={[
|
||||
|
|
@ -629,7 +630,7 @@ const AuthScreen: React.FC = () => {
|
|||
}}>
|
||||
Continue without an account
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</Animated.View>
|
||||
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import {
|
|||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
ActivityIndicator,
|
||||
StatusBar,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
|
@ -16,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons';
|
|||
import { TMDBService } from '../services/tmdbService';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const BACKDROP_WIDTH = width * 0.9;
|
||||
|
|
@ -116,12 +117,16 @@ const BackdropGalleryScreen: React.FC = () => {
|
|||
|
||||
const renderHeader = () => (
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="icon"
|
||||
focusBorderRadius={999}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>
|
||||
{title}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
SafeAreaView,
|
||||
|
|
@ -21,6 +20,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
|||
import { logger } from '../utils/logger';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { useBackupOptions } from '../hooks/useBackupOptions';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
// Check if running on TV platform
|
||||
const isTV = Platform.isTV;
|
||||
|
|
@ -303,13 +303,13 @@ const BackupScreen: React.FC = () => {
|
|||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Empty for now, but keeping structure consistent */}
|
||||
|
|
@ -345,7 +345,7 @@ const BackupScreen: React.FC = () => {
|
|||
</Text>
|
||||
|
||||
{/* Core Data Group */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.sectionHeader}
|
||||
onPress={() => toggleSection('coreData')}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -365,7 +365,7 @@ const BackupScreen: React.FC = () => {
|
|||
>
|
||||
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<Animated.View
|
||||
style={{
|
||||
maxHeight: coreDataAnim.interpolate({
|
||||
|
|
@ -393,7 +393,7 @@ const BackupScreen: React.FC = () => {
|
|||
</Animated.View>
|
||||
|
||||
{/* Addons & Integrations Group */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.sectionHeader}
|
||||
onPress={() => toggleSection('addonsIntegrations')}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -413,7 +413,7 @@ const BackupScreen: React.FC = () => {
|
|||
>
|
||||
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<Animated.View
|
||||
style={{
|
||||
maxHeight: addonsAnim.interpolate({
|
||||
|
|
@ -448,7 +448,7 @@ const BackupScreen: React.FC = () => {
|
|||
</Animated.View>
|
||||
|
||||
{/* Settings & Preferences Group */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.sectionHeader}
|
||||
onPress={() => toggleSection('settingsPreferences')}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -468,7 +468,7 @@ const BackupScreen: React.FC = () => {
|
|||
>
|
||||
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<Animated.View
|
||||
style={{
|
||||
maxHeight: settingsAnim.interpolate({
|
||||
|
|
@ -516,7 +516,7 @@ const BackupScreen: React.FC = () => {
|
|||
Backup & Restore
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
{
|
||||
|
|
@ -535,9 +535,9 @@ const BackupScreen: React.FC = () => {
|
|||
<Text style={styles.actionButtonText}>Create Backup</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
{
|
||||
|
|
@ -550,7 +550,7 @@ const BackupScreen: React.FC = () => {
|
|||
>
|
||||
<MaterialIcons name="restore" size={20} color="white" />
|
||||
<Text style={styles.actionButtonText}>Restore from Backup</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Info Section */}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
|
|
@ -42,6 +41,7 @@ import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
|||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
|
||||
import { tmdbService } from '../services/tmdbService';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
type CatalogScreenProps = {
|
||||
route: RouteProp<RootStackParamList, 'Catalog'>;
|
||||
|
|
@ -762,7 +762,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
const aspectRatio = shape === 'landscape' ? 16 / 9 : (shape === 'square' ? 1 : 2 / 3);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.item,
|
||||
{
|
||||
|
|
@ -772,6 +772,9 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
]}
|
||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type, addonId })}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="poster"
|
||||
focusBorderRadius={12}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: optimizePosterUrl(item.poster) }}
|
||||
|
|
@ -837,7 +840,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
{item.name}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
}, [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}>
|
||||
No content found
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={handleRefresh}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<Text style={styles.buttonText}>Try Again</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
@ -862,12 +869,16 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<Text style={styles.errorText}>
|
||||
{error}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={() => loadItems(true)}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<Text style={styles.buttonText}>Retry</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
@ -885,13 +896,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>{displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
{renderLoadingState()}
|
||||
|
|
@ -904,13 +919,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||
{renderErrorState()}
|
||||
|
|
@ -922,13 +941,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="light-content" />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||
<Text style={styles.backText}>Back</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
<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 => (
|
||||
<React.Fragment key={extra.name}>
|
||||
{/* All option - clears filter */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.filterChip,
|
||||
(extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipActive
|
||||
]}
|
||||
onPress={() => handleFilterChange(extra.name, undefined)}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<Text style={[
|
||||
styles.filterChipText,
|
||||
(extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipTextActive
|
||||
]}>All</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{/* Filter options from catalog extra */}
|
||||
{extra.options?.map(option => {
|
||||
|
|
@ -962,15 +988,19 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
? activeGenreFilter === option
|
||||
: selectedFilters[extra.name] === option;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={option}
|
||||
style={[styles.filterChip, isActive && styles.filterChipActive]}
|
||||
onPress={() => handleFilterChange(extra.name, option)}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={16}
|
||||
hasTVPreferredFocus={Platform.isTV && isActive}
|
||||
>
|
||||
<Text style={[styles.filterChipText, isActive && styles.filterChipTextActive]}>
|
||||
{option}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
Platform,
|
||||
|
|
@ -17,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons';
|
|||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
// TTL options in milliseconds - organized in rows
|
||||
const TTL_OPTIONS = [
|
||||
|
|
@ -132,7 +132,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
|||
const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => {
|
||||
const isSelected = settings.streamCacheTTL === option.value;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.ttlOption,
|
||||
{
|
||||
|
|
@ -142,6 +142,10 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
|||
]}
|
||||
onPress={() => handleUpdateSetting('streamCacheTTL', option.value)}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={8}
|
||||
hasTVPreferredFocus={Platform.isTV && isSelected}
|
||||
>
|
||||
<Text style={[
|
||||
styles.ttlOptionText,
|
||||
|
|
@ -152,7 +156,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
|||
{isSelected && (
|
||||
<MaterialIcons name="check" size={20} color={colors.white} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -162,13 +166,17 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
|||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={handleBack}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={styles.headerTitle}>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
|
|
@ -24,6 +23,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
|||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { fetchContributors, GitHubContributor } from '../services/githubReleaseService';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
|
@ -91,7 +91,7 @@ const ContributorCard: React.FC<ContributorCardProps> = ({ contributor, currentT
|
|||
}, [contributor.html_url]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.contributorCard,
|
||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||
|
|
@ -130,7 +130,7 @@ const ContributorCard: React.FC<ContributorCardProps> = ({ contributor, currentT
|
|||
color={currentTheme.colors.mediumEmphasis}
|
||||
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`;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.contributorCard,
|
||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||
|
|
@ -230,7 +230,7 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
|
|||
color={currentTheme.colors.mediumEmphasis}
|
||||
style={styles.externalIcon}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -422,13 +422,13 @@ const ContributorsScreen: React.FC = () => {
|
|||
<StatusBar barStyle={'light-content'} />
|
||||
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.headerTitle,
|
||||
|
|
@ -457,13 +457,13 @@ const ContributorsScreen: React.FC = () => {
|
|||
|
||||
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.headerTitle,
|
||||
|
|
@ -480,7 +480,7 @@ const ContributorsScreen: React.FC = () => {
|
|||
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||
isTablet && styles.tabletTabSwitcher
|
||||
]}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
activeTab === 'contributors' && { backgroundColor: currentTheme.colors.primary },
|
||||
|
|
@ -496,8 +496,8 @@ const ContributorsScreen: React.FC = () => {
|
|||
]}>
|
||||
Contributors
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
activeTab === 'special' && { backgroundColor: currentTheme.colors.primary },
|
||||
|
|
@ -513,7 +513,7 @@ const ContributorsScreen: React.FC = () => {
|
|||
]}>
|
||||
Special Mentions
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
|
|
@ -530,14 +530,14 @@ const ContributorsScreen: React.FC = () => {
|
|||
<Text style={[styles.errorSubtext, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
GitHub API rate limit exceeded. Please try again later or pull to refresh.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => loadContributors()}
|
||||
>
|
||||
<Text style={[styles.retryText, { color: currentTheme.colors.white }]}>
|
||||
Try Again
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
) : contributors.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
Text,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
Platform,
|
||||
|
|
@ -27,6 +26,7 @@ import { logger } from '../utils/logger';
|
|||
import CustomAlert from '../components/CustomAlert';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import axios from 'axios';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
const TORBOX_STORAGE_KEY = 'torbox_debrid_config';
|
||||
|
|
@ -1132,15 +1132,18 @@ const DebridIntegrationScreen = () => {
|
|||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.actionButton, styles.dangerButton, loading && styles.disabledButton]}
|
||||
onPress={handleDisconnect}
|
||||
disabled={loading}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{loading ? 'Disconnecting...' : 'Disconnect & Remove'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{userData && (
|
||||
<View style={styles.userDataCard}>
|
||||
|
|
@ -1213,12 +1216,15 @@ const DebridIntegrationScreen = () => {
|
|||
<Text style={styles.sectionText}>
|
||||
Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.subscribeButton}
|
||||
onPress={() => Linking.openURL('https://torbox.app/settings?section=integration-settings')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<Text style={styles.subscribeButtonText}>Open Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</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.
|
||||
</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>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Torbox API Key</Text>
|
||||
|
|
@ -1245,24 +1257,33 @@ const DebridIntegrationScreen = () => {
|
|||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.connectButton, loading && styles.disabledButton]}
|
||||
onPress={handleConnect}
|
||||
disabled={loading}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<Text style={styles.connectButtonText}>
|
||||
{loading ? 'Connecting...' : 'Connect & Install'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Unlock Premium Speeds</Text>
|
||||
<Text style={styles.sectionText}>
|
||||
Get a Torbox subscription to access cached high-quality streams with zero buffering.
|
||||
</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>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -1306,12 +1327,15 @@ const DebridIntegrationScreen = () => {
|
|||
<Text style={styles.promoText}>
|
||||
Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.promoButton}
|
||||
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>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
@ -1320,13 +1344,17 @@ const DebridIntegrationScreen = () => {
|
|||
<Text style={styles.configSectionTitle}>Debrid Service *</Text>
|
||||
<View style={styles.pickerContainer}>
|
||||
{TORRENTIO_DEBRID_SERVICES.map((service: any) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={service.id}
|
||||
style={[
|
||||
styles.pickerItem,
|
||||
torrentioConfig.debridService === service.id && styles.pickerItemSelected
|
||||
]}
|
||||
onPress={() => setTorrentioConfig(prev => ({ ...prev, debridService: service.id }))}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV && torrentioConfig.debridService === service.id}
|
||||
>
|
||||
<Text style={[
|
||||
styles.pickerItemText,
|
||||
|
|
@ -1334,7 +1362,7 @@ const DebridIntegrationScreen = () => {
|
|||
]}>
|
||||
{service.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -1355,9 +1383,12 @@ const DebridIntegrationScreen = () => {
|
|||
</View>
|
||||
|
||||
{/* Sorting - Accordion */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.accordionHeader, expandedSections.sorting && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||
onPress={() => toggleSection('sorting')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<View>
|
||||
<Text style={styles.accordionHeaderText}>Sorting</Text>
|
||||
|
|
@ -1366,29 +1397,36 @@ const DebridIntegrationScreen = () => {
|
|||
</Text>
|
||||
</View>
|
||||
<Feather name={expandedSections.sorting ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{expandedSections.sorting && (
|
||||
<View style={styles.accordionContent}>
|
||||
<View style={styles.pickerContainer}>
|
||||
{TORRENTIO_SORT_OPTIONS.map(option => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={option.id}
|
||||
style={[styles.pickerItem, torrentioConfig.sort === option.id && styles.pickerItemSelected]}
|
||||
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]}>
|
||||
{option.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Quality Filter - Accordion */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.accordionHeader, expandedSections.qualityFilter && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||
onPress={() => toggleSection('qualityFilter')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<View>
|
||||
<Text style={styles.accordionHeaderText}>Exclude Qualities</Text>
|
||||
|
|
@ -1397,29 +1435,36 @@ const DebridIntegrationScreen = () => {
|
|||
</Text>
|
||||
</View>
|
||||
<Feather name={expandedSections.qualityFilter ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{expandedSections.qualityFilter && (
|
||||
<View style={styles.accordionContent}>
|
||||
<View style={styles.chipContainer}>
|
||||
{TORRENTIO_QUALITY_FILTERS.map(quality => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={quality.id}
|
||||
style={[styles.chip, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipSelected]}
|
||||
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]}>
|
||||
{quality.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Priority Languages - Accordion */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.accordionHeader, expandedSections.languages && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||
onPress={() => toggleSection('languages')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<View>
|
||||
<Text style={styles.accordionHeaderText}>Priority Languages</Text>
|
||||
|
|
@ -1428,29 +1473,36 @@ const DebridIntegrationScreen = () => {
|
|||
</Text>
|
||||
</View>
|
||||
<Feather name={expandedSections.languages ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{expandedSections.languages && (
|
||||
<View style={styles.accordionContent}>
|
||||
<View style={styles.chipContainer}>
|
||||
{TORRENTIO_LANGUAGES.map(lang => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={lang.id}
|
||||
style={[styles.chip, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipSelected]}
|
||||
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]}>
|
||||
{lang.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Max Results - Accordion */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.accordionHeader, expandedSections.maxResults && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||
onPress={() => toggleSection('maxResults')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<View>
|
||||
<Text style={styles.accordionHeaderText}>Max Results</Text>
|
||||
|
|
@ -1459,36 +1511,43 @@ const DebridIntegrationScreen = () => {
|
|||
</Text>
|
||||
</View>
|
||||
<Feather name={expandedSections.maxResults ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{expandedSections.maxResults && (
|
||||
<View style={styles.accordionContent}>
|
||||
<View style={styles.pickerContainer}>
|
||||
{TORRENTIO_MAX_RESULTS.map(option => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={option.id || 'all'}
|
||||
style={[styles.pickerItem, torrentioConfig.maxResults === option.id && styles.pickerItemSelected]}
|
||||
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]}>
|
||||
{option.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Additional Options - Accordion */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.accordionHeader, expandedSections.options && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||
onPress={() => toggleSection('options')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<View>
|
||||
<Text style={styles.accordionHeaderText}>Additional Options</Text>
|
||||
<Text style={styles.accordionSubtext}>Catalog & download settings</Text>
|
||||
</View>
|
||||
<Feather name={expandedSections.options ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{expandedSections.options && (
|
||||
<View style={styles.accordionContent}>
|
||||
<View style={styles.switchRow}>
|
||||
|
|
@ -1526,33 +1585,42 @@ const DebridIntegrationScreen = () => {
|
|||
<View style={{ marginTop: 8 }}>
|
||||
{torrentioConfig.isInstalled ? (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.connectButton, torrentioLoading && styles.disabledButton]}
|
||||
onPress={handleInstallTorrentio}
|
||||
disabled={torrentioLoading}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<Text style={styles.connectButtonText}>
|
||||
{torrentioLoading ? 'Updating...' : 'Update Configuration'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.actionButton, styles.dangerButton, torrentioLoading && styles.disabledButton]}
|
||||
onPress={handleRemoveTorrentio}
|
||||
disabled={torrentioLoading}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<Text style={styles.buttonText}>Remove Torrentio</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.connectButton, torrentioLoading && styles.disabledButton]}
|
||||
onPress={handleInstallTorrentio}
|
||||
disabled={torrentioLoading}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<Text style={styles.connectButtonText}>
|
||||
{torrentioLoading ? 'Installing...' : 'Install Torrentio'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
|
@ -1578,33 +1646,45 @@ const DebridIntegrationScreen = () => {
|
|||
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="icon"
|
||||
focusBorderRadius={999}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<Feather name="arrow-left" size={24} color={colors.white} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<Text style={styles.headerTitle}>Debrid Integration</Text>
|
||||
</View>
|
||||
|
||||
{/* Tab Selector */}
|
||||
<View style={styles.tabContainer}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.tab, activeTab === 'torbox' && styles.activeTab]}
|
||||
onPress={() => setActiveTab('torbox')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={16}
|
||||
hasTVPreferredFocus={Platform.isTV && activeTab === 'torbox'}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTab === 'torbox' && styles.activeTabText]}>
|
||||
TorBox
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.tab, activeTab === 'torrentio' && styles.activeTab]}
|
||||
onPress={() => setActiveTab('torrentio')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={16}
|
||||
hasTVPreferredFocus={Platform.isTV && activeTab === 'torrentio'}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTab === 'torrentio' && styles.activeTabText]}>
|
||||
Torrentio
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
StyleSheet,
|
||||
StatusBar,
|
||||
Dimensions,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
RefreshControl,
|
||||
Alert,
|
||||
|
|
@ -35,6 +34,7 @@ import type { DownloadItem } from '../contexts/DownloadsContext';
|
|||
import { useToast } from '../contexts/ToastContext';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import ScreenHeader from '../components/common/ScreenHeader';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { height, width } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
|
@ -80,7 +80,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
|
|||
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Downloaded content will appear here for offline viewing
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => {
|
||||
navigation.navigate('Search');
|
||||
|
|
@ -89,7 +89,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
|
|||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.background }]}>
|
||||
Explore Content
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
@ -204,7 +204,7 @@ const DownloadItemComponent: React.FC<{
|
|||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.downloadItem, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={() => onPress(item)}
|
||||
onLongPress={handleLongPress}
|
||||
|
|
@ -314,7 +314,7 @@ const DownloadItemComponent: React.FC<{
|
|||
{/* Action buttons */}
|
||||
<View style={styles.actionContainer}>
|
||||
{getActionIcon() && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={handleActionPress}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -324,10 +324,10 @@ const DownloadItemComponent: React.FC<{
|
|||
size={20}
|
||||
color={currentTheme.colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={() => onRequestRemove(item)}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -337,9 +337,9 @@ const DownloadItemComponent: React.FC<{
|
|||
size={20}
|
||||
color={currentTheme.colors.error}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -568,7 +568,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
);
|
||||
|
||||
const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={filter}
|
||||
style={[
|
||||
styles.filterButton,
|
||||
|
|
@ -612,7 +612,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -627,7 +627,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
<ScreenHeader
|
||||
title="Downloads"
|
||||
rightActionComponent={
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.helpButton}
|
||||
onPress={showDownloadHelp}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -637,7 +637,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
size={24}
|
||||
color={currentTheme.colors.mediumEmphasis}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
}
|
||||
isTablet={isTablet}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
|
|
@ -77,6 +76,7 @@ import { useToast } from '../contexts/ToastContext';
|
|||
import FirstTimeWelcome from '../components/FirstTimeWelcome';
|
||||
import { HeaderVisibility } from '../contexts/HeaderVisibility';
|
||||
import { useTrailer } from '../contexts/TrailerContext';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
// Constants
|
||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||
|
|
@ -799,15 +799,18 @@ const HomeScreen = () => {
|
|||
return (
|
||||
<View>
|
||||
<View style={styles.loadMoreContainer}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.loadMoreButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={handleLoadMoreCatalogs}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.loadMoreText, { color: currentTheme.colors.white }]}>
|
||||
Load More Catalogs
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -829,13 +832,17 @@ const HomeScreen = () => {
|
|||
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||
No content available
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={16}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
useWindowDimensions,
|
||||
SafeAreaView,
|
||||
|
|
@ -38,6 +37,7 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
|||
import { traktService, TraktService, TraktImages } from '../services/traktService';
|
||||
import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
interface LibraryItem extends StreamingContent {
|
||||
progress?: number;
|
||||
|
|
@ -121,10 +121,13 @@ const TraktItem = React.memo(({
|
|||
}, [navigation, item.imdbId, item.type]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.itemContainer, { width }]}
|
||||
onPress={handlePress}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="poster"
|
||||
focusBorderRadius={12}
|
||||
>
|
||||
<View>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
|
|
@ -146,7 +149,7 @@ const TraktItem = React.memo(({
|
|||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -386,7 +389,7 @@ const LibraryScreen = () => {
|
|||
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
||||
|
||||
const renderItem = ({ item }: { item: LibraryItem }) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.itemContainer, { width: itemWidth }]}
|
||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||
onLongPress={() => {
|
||||
|
|
@ -394,6 +397,9 @@ const LibraryScreen = () => {
|
|||
setMenuVisible(true);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="poster"
|
||||
focusBorderRadius={12}
|
||||
>
|
||||
<View>
|
||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||
|
|
@ -424,17 +430,20 @@ const LibraryScreen = () => {
|
|||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
|
||||
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.itemContainer, { width: itemWidth }]}
|
||||
onPress={() => {
|
||||
setSelectedTraktFolder(folder.id);
|
||||
loadAllCollections();
|
||||
}}
|
||||
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.folderGradient}>
|
||||
|
|
@ -452,11 +461,11 @@ const LibraryScreen = () => {
|
|||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
|
||||
const renderTraktFolder = () => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.itemContainer, { width: itemWidth }]}
|
||||
onPress={() => {
|
||||
if (!traktAuthenticated) {
|
||||
|
|
@ -468,6 +477,9 @@ const LibraryScreen = () => {
|
|||
}
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="card"
|
||||
focusBorderRadius={12}
|
||||
>
|
||||
<View>
|
||||
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
|
|
@ -489,7 +501,7 @@ const LibraryScreen = () => {
|
|||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
|
||||
const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => {
|
||||
|
|
@ -715,7 +727,7 @@ const LibraryScreen = () => {
|
|||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
Your Trakt collections will appear here once you start using Trakt
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
|
|
@ -724,9 +736,13 @@ const LibraryScreen = () => {
|
|||
loadAllCollections();
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={16}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Load Collections</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -756,7 +772,7 @@ const LibraryScreen = () => {
|
|||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
This collection is empty
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
|
|
@ -765,9 +781,13 @@ const LibraryScreen = () => {
|
|||
loadAllCollections();
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={16}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Refresh</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -791,7 +811,7 @@ const LibraryScreen = () => {
|
|||
const isActive = filter === filterType;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.filterButton,
|
||||
isActive && { backgroundColor: currentTheme.colors.primary },
|
||||
|
|
@ -811,6 +831,10 @@ const LibraryScreen = () => {
|
|||
setFilter(filterType);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={18}
|
||||
hasTVPreferredFocus={Platform.isTV && isActive}
|
||||
>
|
||||
{filterType === 'trakt' ? (
|
||||
<View style={[styles.filterIcon, { justifyContent: 'center', alignItems: 'center' }]}>
|
||||
|
|
@ -833,7 +857,7 @@ const LibraryScreen = () => {
|
|||
>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -858,16 +882,20 @@ const LibraryScreen = () => {
|
|||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
{emptySubtitle}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
backgroundColor: currentTheme.colors.primary,
|
||||
shadowColor: currentTheme.colors.black
|
||||
}]}
|
||||
onPress={() => navigation.navigate('Search')}
|
||||
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>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
StatusBar,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
TouchableOpacity,
|
||||
InteractionManager,
|
||||
BackHandler,
|
||||
Platform,
|
||||
|
|
@ -64,6 +63,7 @@ import { useWatchProgress } from '../hooks/useWatchProgress';
|
|||
import { TraktService, TraktPlaybackItem } from '../services/traktService';
|
||||
import { tmdbService } from '../services/tmdbService';
|
||||
import { catalogService } from '../services/catalogService';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { height } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -865,21 +865,28 @@ const MetadataScreen: React.FC = () => {
|
|||
{metadataError}
|
||||
</Text>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={loadMetadata}
|
||||
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 }} />
|
||||
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.backButton, { borderColor: currentTheme.colors.primary }]}
|
||||
onPress={handleBack}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</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 */}
|
||||
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length === 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
|
||||
<View style={styles.backdropGalleryContainer}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backdropGalleryButton}
|
||||
onPress={() => navigation.navigate('BackdropGallery' as any, {
|
||||
tmdbId: metadata.tmdbId,
|
||||
|
|
@ -1248,10 +1255,13 @@ const MetadataScreen: React.FC = () => {
|
|||
title: metadata.name || 'Gallery'
|
||||
})}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</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 */}
|
||||
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
|
||||
<View style={styles.backdropGalleryContainer}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backdropGalleryButton}
|
||||
onPress={() => navigation.navigate('BackdropGallery' as any, {
|
||||
tmdbId: metadata.tmdbId,
|
||||
|
|
@ -1389,10 +1399,13 @@ const MetadataScreen: React.FC = () => {
|
|||
title: metadata.name || 'Gallery'
|
||||
})}
|
||||
focusable={Platform.isTV}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={16}
|
||||
>
|
||||
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
Text,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
TouchableOpacity,
|
||||
StatusBar,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
|
@ -25,6 +24,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
|||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -281,9 +281,9 @@ const OnboardingScreen = () => {
|
|||
entering={FadeIn.delay(300).duration(600)}
|
||||
style={styles.header}
|
||||
>
|
||||
<TouchableOpacity onPress={handleSkip} style={styles.skipButton}>
|
||||
<FocusableTouchableOpacity onPress={handleSkip} style={styles.skipButton}>
|
||||
<Text style={styles.skipText}>Skip</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{/* Smooth Progress Bar */}
|
||||
<View style={styles.progressContainer}>
|
||||
|
|
@ -322,7 +322,7 @@ const OnboardingScreen = () => {
|
|||
</View>
|
||||
|
||||
{/* Animated Button */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={handleNext}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
|
|
@ -333,7 +333,7 @@ const OnboardingScreen = () => {
|
|||
{currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Continue'}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
ScrollView,
|
||||
SafeAreaView,
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
StatusBar,
|
||||
Switch,
|
||||
} from 'react-native';
|
||||
|
|
@ -15,6 +14,7 @@ import { useSettings, AppSettings } from '../hooks/useSettings';
|
|||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
const { currentTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
style={[
|
||||
|
|
@ -87,7 +87,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
/>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ const PlayerSettingsScreen: React.FC = () => {
|
|||
/>
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={handleBack}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -186,7 +186,7 @@ const PlayerSettingsScreen: React.FC = () => {
|
|||
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
||||
Settings
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Empty for now, but ready for future actions */}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Switch,
|
||||
TextInput,
|
||||
ScrollView,
|
||||
|
|
@ -25,6 +24,7 @@ import { useSettings } from '../hooks/useSettings';
|
|||
import { localScraperService, pluginService, ScraperInfo, RepositoryInfo } from '../services/pluginService';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -773,23 +773,23 @@ const CollapsibleSection: React.FC<{
|
|||
styles: any;
|
||||
}> = ({ title, children, isExpanded, onToggle, colors, styles }) => (
|
||||
<View style={styles.collapsibleSection}>
|
||||
<TouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
|
||||
<FocusableTouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
|
||||
<Text style={styles.collapsibleTitle}>{title}</Text>
|
||||
<Ionicons
|
||||
name={isExpanded ? "chevron-up" : "chevron-down"}
|
||||
size={20}
|
||||
color={colors.mediumGray}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
{isExpanded && <View style={styles.collapsibleContent}>{children}</View>}
|
||||
</View>
|
||||
);
|
||||
|
||||
// Helper component for info tooltips
|
||||
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} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
// Helper component for status badges
|
||||
|
|
@ -1361,22 +1361,22 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<Ionicons name="arrow-back" size={24} color={colors.primary} />
|
||||
<Text style={styles.backText}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Help Button */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={() => setShowHelpModal(true)}
|
||||
>
|
||||
<Ionicons name="help-circle-outline" size={20} color={colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -1485,7 +1485,7 @@ const PluginsScreen: React.FC = () => {
|
|||
</View>
|
||||
<View style={styles.repositoryActions}>
|
||||
{repo.id !== currentRepositoryId && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonPrimary]}
|
||||
onPress={() => handleSwitchRepository(repo.id)}
|
||||
disabled={switchingRepository === repo.id}
|
||||
|
|
@ -1495,9 +1495,9 @@ const PluginsScreen: React.FC = () => {
|
|||
) : (
|
||||
<Text style={styles.repositoryActionButtonText}>Switch</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonSecondary]}
|
||||
onPress={() => handleRefreshRepository()}
|
||||
disabled={isRefreshing || switchingRepository !== null}
|
||||
|
|
@ -1507,14 +1507,14 @@ const PluginsScreen: React.FC = () => {
|
|||
) : (
|
||||
<Text style={styles.repositoryActionButtonText}>Refresh</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonDanger]}
|
||||
onPress={() => handleRemoveRepository(repo.id)}
|
||||
disabled={switchingRepository !== null}
|
||||
>
|
||||
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
|
@ -1523,13 +1523,13 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
|
||||
{/* Add Repository Button */}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.button, styles.primaryButton, { marginTop: 16 }]}
|
||||
onPress={() => setShowAddRepositoryModal(true)}
|
||||
disabled={!settings.enableLocalScrapers || switchingRepository !== null}
|
||||
>
|
||||
<Text style={styles.buttonText}>Add New Repository</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</CollapsibleSection>
|
||||
|
||||
{/* Available Plugins */}
|
||||
|
|
@ -1553,16 +1553,16 @@ const PluginsScreen: React.FC = () => {
|
|||
placeholderTextColor={colors.mediumGray}
|
||||
/>
|
||||
{searchQuery.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
||||
<FocusableTouchableOpacity onPress={() => setSearchQuery('')}>
|
||||
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Filter Chips */}
|
||||
<View style={styles.filterContainer}>
|
||||
{['all', 'movie', 'tv'].map((filter) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={filter}
|
||||
style={[
|
||||
styles.filterChip,
|
||||
|
|
@ -1576,27 +1576,27 @@ const PluginsScreen: React.FC = () => {
|
|||
]}>
|
||||
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Bulk Actions */}
|
||||
{filteredScrapers.length > 0 && (
|
||||
<View style={styles.bulkActionsContainer}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.bulkActionButton, styles.bulkActionButtonEnabled]}
|
||||
onPress={() => handleBulkToggle(true)}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<Text style={[styles.bulkActionButtonText, { color: '#34C759' }]}>Enable All</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.bulkActionButton, styles.bulkActionButtonDisabled]}
|
||||
onPress={() => handleBulkToggle(false)}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<Text style={[styles.bulkActionButtonText, { color: colors.mediumGray }]}>Disable All</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -1620,12 +1620,12 @@ const PluginsScreen: React.FC = () => {
|
|||
}
|
||||
</Text>
|
||||
{searchQuery && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={() => setSearchQuery('')}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
|
|
@ -1713,14 +1713,14 @@ const PluginsScreen: React.FC = () => {
|
|||
numberOfLines={1}
|
||||
/>
|
||||
{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} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.buttonRow}>
|
||||
{showboxUiToken !== showboxSavedToken && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.button, styles.primaryButton]}
|
||||
onPress={async () => {
|
||||
if (showboxScraperId) {
|
||||
|
|
@ -1731,9 +1731,9 @@ const PluginsScreen: React.FC = () => {
|
|||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.button, styles.secondaryButton]}
|
||||
onPress={async () => {
|
||||
setShowboxUiToken('');
|
||||
|
|
@ -1744,7 +1744,7 @@ const PluginsScreen: React.FC = () => {
|
|||
}}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -1849,7 +1849,7 @@ const PluginsScreen: React.FC = () => {
|
|||
{qualityOptions.map((quality) => {
|
||||
const isExcluded = (settings.excludedQualities || []).includes(quality);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={quality}
|
||||
style={[
|
||||
styles.qualityChip,
|
||||
|
|
@ -1866,7 +1866,7 @@ const PluginsScreen: React.FC = () => {
|
|||
]}>
|
||||
{isExcluded ? '✕ ' : ''}{quality}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
|
@ -1898,7 +1898,7 @@ const PluginsScreen: React.FC = () => {
|
|||
{languageOptions.map((language) => {
|
||||
const isExcluded = (settings.excludedLanguages || []).includes(language);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={language}
|
||||
style={[
|
||||
styles.qualityChip,
|
||||
|
|
@ -1915,7 +1915,7 @@ const PluginsScreen: React.FC = () => {
|
|||
]}>
|
||||
{isExcluded ? '✕ ' : ''}{language}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
|
@ -1964,12 +1964,12 @@ const PluginsScreen: React.FC = () => {
|
|||
<Text style={styles.modalText}>
|
||||
4. <Text style={{ fontWeight: '600' }}>Enable Scrapers</Text> - Turn on the scrapers you want to use for streaming
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.modalButton}
|
||||
onPress={() => setShowHelpModal(false)}
|
||||
>
|
||||
<Text style={styles.modalButtonText}>Got it!</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
|
@ -2011,7 +2011,7 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.compactActions}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.compactButton, styles.cancelButton]}
|
||||
onPress={() => {
|
||||
setShowAddRepositoryModal(false);
|
||||
|
|
@ -2019,9 +2019,9 @@ const PluginsScreen: React.FC = () => {
|
|||
}}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
||||
onPress={handleAddRepository}
|
||||
disabled={!newRepositoryUrl.trim() || isLoading}
|
||||
|
|
@ -2031,7 +2031,7 @@ const PluginsScreen: React.FC = () => {
|
|||
) : (
|
||||
<Text style={styles.addButtonText}>Add</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
StatusBar,
|
||||
Platform,
|
||||
|
|
@ -17,6 +16,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
|||
import { useTraktContext } from '../contexts/TraktContext';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
const PROFILE_STORAGE_KEY = 'user_profiles';
|
||||
|
|
@ -183,7 +183,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
|
||||
const renderItem = ({ item }: { item: Profile }) => (
|
||||
<View style={styles.profileItem}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.profileContent,
|
||||
item.isActive && {
|
||||
|
|
@ -211,14 +211,14 @@ const ProfilesScreen: React.FC = () => {
|
|||
)}
|
||||
</View>
|
||||
{!item.isActive && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={() => handleDeleteProfile(item.id)}
|
||||
>
|
||||
<MaterialIcons name="delete" size={24} color={currentTheme.colors.error} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
@ -227,7 +227,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||||
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={handleBack}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
|
|
@ -237,7 +237,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
size={24}
|
||||
color={currentTheme.colors.text}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<Text
|
||||
style={[
|
||||
styles.headerTitle,
|
||||
|
|
@ -260,7 +260,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
</Text>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.addButton,
|
||||
{ backgroundColor: currentTheme.colors.elevation2 }
|
||||
|
|
@ -271,7 +271,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
<Text style={[styles.addButtonText, { color: currentTheme.colors.text }]}>
|
||||
Add New Profile
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
|
@ -307,7 +307,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
/>
|
||||
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.modalButton, styles.cancelButton]}
|
||||
onPress={() => {
|
||||
setNewProfileName('');
|
||||
|
|
@ -315,8 +315,8 @@ const ProfilesScreen: React.FC = () => {
|
|||
}}
|
||||
>
|
||||
<Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.modalButton,
|
||||
styles.createButton,
|
||||
|
|
@ -325,7 +325,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
onPress={handleAddProfile}
|
||||
>
|
||||
<Text style={{ color: '#fff' }}>Create</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
StyleSheet,
|
||||
TextInput,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
useColorScheme,
|
||||
SafeAreaView,
|
||||
|
|
@ -44,6 +43,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import LoadingSpinner from '../components/common/LoadingSpinner';
|
||||
import ScreenHeader from '../components/common/ScreenHeader';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ const MAX_RECENT_SEARCHES = 10;
|
|||
|
||||
const PLACEHOLDER_POSTER = 'https://placehold.co/300x450/222222/CCCCCC?text=No+Poster';
|
||||
|
||||
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
|
||||
// NOTE: AnimatedTouchable was unused; keep focus wrapper for TV instead.
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||
|
|
@ -570,24 +570,29 @@ const SearchScreen = () => {
|
|||
Recent Searches
|
||||
</Text>
|
||||
{recentSearches.map((search, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={styles.recentSearchItem}
|
||||
onPress={() => {
|
||||
setQuery(search);
|
||||
Keyboard.dismiss();
|
||||
}}
|
||||
>
|
||||
<View key={index} style={styles.recentSearchItem}>
|
||||
<MaterialIcons
|
||||
name="history"
|
||||
size={20}
|
||||
color={currentTheme.colors.lightGray}
|
||||
style={styles.recentSearchIcon}
|
||||
/>
|
||||
<FocusableTouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
onPress={() => {
|
||||
setQuery(search);
|
||||
Keyboard.dismiss();
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={12}
|
||||
>
|
||||
<Text style={[styles.recentSearchText, { color: currentTheme.colors.white }]}>
|
||||
{search}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => {
|
||||
const newRecentSearches = [...recentSearches];
|
||||
newRecentSearches.splice(index, 1);
|
||||
|
|
@ -596,10 +601,15 @@ const SearchScreen = () => {
|
|||
}}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
style={styles.recentSearchDeleteButton}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="icon"
|
||||
focusBorderRadius={12}
|
||||
hasTVPreferredFocus={Platform.isTV && index === 0}
|
||||
>
|
||||
<MaterialIcons name="close" size={16} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
|
|
@ -651,7 +661,7 @@ const SearchScreen = () => {
|
|||
}, [item.id, item.type]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.horizontalItem, { width: itemWidth }]}
|
||||
onPress={() => {
|
||||
navigation.navigate('Metadata', { id: item.id, type: item.type });
|
||||
|
|
@ -663,6 +673,9 @@ const SearchScreen = () => {
|
|||
}}
|
||||
delayLongPress={300}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="poster"
|
||||
focusBorderRadius={12}
|
||||
>
|
||||
<View style={[styles.horizontalItemPosterContainer, {
|
||||
width: itemWidth,
|
||||
|
|
@ -716,7 +729,7 @@ const SearchScreen = () => {
|
|||
{item.year}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -934,17 +947,21 @@ const SearchScreen = () => {
|
|||
ref={inputRef}
|
||||
/>
|
||||
{query.length > 0 && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={handleClearSearch}
|
||||
style={styles.clearButton}
|
||||
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="icon"
|
||||
focusBorderRadius={999}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="close"
|
||||
size={20}
|
||||
color={currentTheme.colors.lightGray}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Switch,
|
||||
ScrollView,
|
||||
SafeAreaView,
|
||||
|
|
@ -56,6 +55,7 @@ import TraktIcon from '../components/icons/TraktIcon';
|
|||
import TMDBIcon from '../components/icons/TMDBIcon';
|
||||
import MDBListIcon from '../components/icons/MDBListIcon';
|
||||
import { campaignService } from '../services/campaignService';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
|
@ -144,7 +144,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
const { currentTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
activeOpacity={0.6}
|
||||
onPress={onPress}
|
||||
style={[
|
||||
|
|
@ -153,6 +153,9 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
{ borderBottomColor: currentTheme.colors.elevation2 },
|
||||
isTablet && styles.tabletSettingItem
|
||||
]}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={isTablet ? 18 : 16}
|
||||
>
|
||||
<View style={[
|
||||
styles.settingIconContainer,
|
||||
|
|
@ -201,7 +204,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
|||
{renderControl()}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -237,7 +240,7 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
|
|||
|
||||
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
||||
{categories.map((category) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={category.id}
|
||||
style={[
|
||||
styles.sidebarItem,
|
||||
|
|
@ -248,6 +251,10 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
|
|||
]}
|
||||
onPress={() => onCategorySelect(category.id)}
|
||||
activeOpacity={0.6}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={14}
|
||||
hasTVPreferredFocus={Platform.isTV && selectedCategory === category.id}
|
||||
>
|
||||
<View style={[
|
||||
styles.sidebarItemIconContainer,
|
||||
|
|
@ -278,7 +285,7 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
|
|||
]}>
|
||||
{category.title}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
|
@ -959,7 +966,7 @@ const SettingsScreen: React.FC = () => {
|
|||
)}
|
||||
|
||||
<View style={styles.discordContainer}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
|
||||
onPress={() => {
|
||||
const url = 'https://ko-fi.com/tapframe';
|
||||
|
|
@ -973,19 +980,25 @@ const SettingsScreen: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<FastImage
|
||||
source={require('../../assets/support_me_on_kofi_red.png')}
|
||||
style={styles.kofiImage}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||
onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<View style={styles.discordButtonContent}>
|
||||
<FastImage
|
||||
|
|
@ -997,12 +1010,15 @@ const SettingsScreen: React.FC = () => {
|
|||
Discord
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]}
|
||||
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<View style={styles.discordButtonContent}>
|
||||
<FastImage
|
||||
|
|
@ -1014,7 +1030,7 @@ const SettingsScreen: React.FC = () => {
|
|||
Reddit
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -1093,7 +1109,7 @@ const SettingsScreen: React.FC = () => {
|
|||
|
||||
{/* Support & Community Buttons */}
|
||||
<View style={styles.discordContainer}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
|
||||
onPress={() => {
|
||||
const url = 'https://ko-fi.com/tapframe';
|
||||
|
|
@ -1107,19 +1123,25 @@ const SettingsScreen: React.FC = () => {
|
|||
}
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<FastImage
|
||||
source={require('../../assets/support_me_on_kofi_red.png')}
|
||||
style={styles.kofiImage}
|
||||
resizeMode={FastImage.resizeMode.contain}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||
onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<View style={styles.discordButtonContent}>
|
||||
<FastImage
|
||||
|
|
@ -1131,12 +1153,15 @@ const SettingsScreen: React.FC = () => {
|
|||
Discord
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]}
|
||||
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={14}
|
||||
>
|
||||
<View style={styles.discordButtonContent}>
|
||||
<FastImage
|
||||
|
|
@ -1148,7 +1173,7 @@ const SettingsScreen: React.FC = () => {
|
|||
Reddit
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
ScrollView,
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
TouchableOpacity,
|
||||
Platform,
|
||||
StatusBar,
|
||||
} from 'react-native';
|
||||
|
|
@ -34,6 +33,7 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
|||
import axios from 'axios';
|
||||
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
||||
import { logger } from '../utils/logger';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
type RootStackParamList = {
|
||||
ShowRatings: { showId: number };
|
||||
|
|
@ -140,7 +140,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
|
|||
{['tmdb', 'imdb', 'tvmaze'].map((source) => {
|
||||
const isActive = ratingSource === source;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={source}
|
||||
style={[
|
||||
styles.sourceButton,
|
||||
|
|
@ -148,6 +148,10 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
|
|||
isActive && { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary }
|
||||
]}
|
||||
onPress={() => setRatingSource(source as RatingSource)}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="pill"
|
||||
focusBorderRadius={8}
|
||||
hasTVPreferredFocus={Platform.isTV && isActive}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
|
|
@ -158,7 +162,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
|
|||
>
|
||||
{source.toUpperCase()}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
SectionList,
|
||||
|
|
@ -58,6 +57,7 @@ import StreamCard from '../components/StreamCard';
|
|||
import AnimatedImage from '../components/AnimatedImage';
|
||||
import AnimatedText from '../components/AnimatedText';
|
||||
import AnimatedView from '../components/AnimatedView';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
// Lazy-safe community blur import for Android
|
||||
let AndroidBlurView: any = null;
|
||||
|
|
@ -1850,19 +1850,23 @@ export const StreamsScreen = () => {
|
|||
<View
|
||||
style={[styles.backButtonContainer, isTablet && styles.backButtonContainerTablet]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.backButton,
|
||||
Platform.OS === 'android' ? { paddingTop: 45 } : null
|
||||
]}
|
||||
onPress={handleBack}
|
||||
activeOpacity={0.7}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="listRow"
|
||||
focusBorderRadius={999}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
||||
<Text style={styles.backButtonText}>
|
||||
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? 'Back to Episodes' : 'Back to Info'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
@ -2105,12 +2109,16 @@ export const StreamsScreen = () => {
|
|||
<Text style={styles.noStreamsSubText}>
|
||||
Please add streaming sources in settings
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.addSourcesButton}
|
||||
onPress={() => navigation.navigate('Addons')}
|
||||
enableTVFocus={Platform.isTV}
|
||||
preset="button"
|
||||
focusBorderRadius={16}
|
||||
hasTVPreferredFocus={Platform.isTV}
|
||||
>
|
||||
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
) : streamsEmpty ? (
|
||||
showInitialLoading ? (
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
|
|
@ -29,6 +28,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
|||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
// (duplicate import removed)
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
||||
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
|
||||
|
|
@ -516,13 +516,13 @@ const TMDBSettingsScreen = () => {
|
|||
<StatusBar barStyle="light-content" />
|
||||
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||
TMDb Settings
|
||||
|
|
@ -592,12 +592,12 @@ const TMDBSettingsScreen = () => {
|
|||
Current: {(settings.tmdbLanguagePreference || 'en').toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => setLanguagePickerVisible(true)}
|
||||
style={[styles.languageButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
>
|
||||
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Logo Preview */}
|
||||
|
|
@ -617,7 +617,7 @@ const TMDBSettingsScreen = () => {
|
|||
style={styles.showsScrollView}
|
||||
>
|
||||
{EXAMPLE_SHOWS.map((show) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={show.imdbId}
|
||||
style={[
|
||||
styles.showItem,
|
||||
|
|
@ -636,7 +636,7 @@ const TMDBSettingsScreen = () => {
|
|||
>
|
||||
{show.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
|
|
@ -725,29 +725,29 @@ const TMDBSettingsScreen = () => {
|
|||
onFocus={() => setIsInputFocused(true)}
|
||||
onBlur={() => setIsInputFocused(false)}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.pasteButton}
|
||||
onPress={pasteFromClipboard}
|
||||
>
|
||||
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.buttonRow}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={saveApiKey}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.white }]}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{isKeySet && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.button, styles.clearButton, { borderColor: currentTheme.colors.error }]}
|
||||
onPress={clearApiKey}
|
||||
>
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.error }]}>Clear</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
|
@ -771,7 +771,7 @@ const TMDBSettingsScreen = () => {
|
|||
</View>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.helpLink}
|
||||
onPress={openTMDBWebsite}
|
||||
>
|
||||
|
|
@ -779,7 +779,7 @@ const TMDBSettingsScreen = () => {
|
|||
<Text style={[styles.helpText, { color: currentTheme.colors.primary }]}>
|
||||
How to get a TMDb API key?
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -805,7 +805,7 @@ const TMDBSettingsScreen = () => {
|
|||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.error }]}
|
||||
onPress={handleClearCache}
|
||||
>
|
||||
|
|
@ -813,7 +813,7 @@ const TMDBSettingsScreen = () => {
|
|||
<MaterialIcons name="delete-outline" size={18} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.buttonText, { color: currentTheme.colors.white, marginLeft: 8 }]}>Clear Cache</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={[styles.infoContainer, { marginTop: 12 }]}>
|
||||
<MaterialIcons name="info-outline" size={18} color={currentTheme.colors.primary} />
|
||||
|
|
@ -856,9 +856,9 @@ const TMDBSettingsScreen = () => {
|
|||
autoCorrect={false}
|
||||
/>
|
||||
{languageSearch.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setLanguageSearch('')} style={styles.searchClearButton}>
|
||||
<FocusableTouchableOpacity onPress={() => setLanguageSearch('')} style={styles.searchClearButton}>
|
||||
<MaterialIcons name="close" size={20} color={currentTheme.colors.mediumEmphasis} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -880,7 +880,7 @@ const TMDBSettingsScreen = () => {
|
|||
{ code: 'de', label: 'DE' },
|
||||
{ code: 'tr', label: 'TR' },
|
||||
].map(({ code, label }) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={code}
|
||||
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
||||
style={[
|
||||
|
|
@ -899,7 +899,7 @@ const TMDBSettingsScreen = () => {
|
|||
]}>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
|
@ -956,7 +956,7 @@ const TMDBSettingsScreen = () => {
|
|||
return (
|
||||
<>
|
||||
{filteredLanguages.map(({ code, label, native }) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
key={code}
|
||||
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
||||
style={[
|
||||
|
|
@ -992,7 +992,7 @@ const TMDBSettingsScreen = () => {
|
|||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
))}
|
||||
{languageSearch.length > 0 && filteredLanguages.length === 0 && (
|
||||
<View style={styles.noResultsContainer}>
|
||||
|
|
@ -1000,12 +1000,12 @@ const TMDBSettingsScreen = () => {
|
|||
<Text style={[styles.noResultsText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
No languages found for "{languageSearch}"
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => setLanguageSearch('')}
|
||||
style={[styles.clearSearchButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||
>
|
||||
<Text style={[styles.clearSearchButtonText, { color: currentTheme.colors.primary }]}>Clear search</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -1016,18 +1016,18 @@ const TMDBSettingsScreen = () => {
|
|||
|
||||
{/* Footer Actions */}
|
||||
<View style={styles.modalFooter}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => setLanguagePickerVisible(false)}
|
||||
style={styles.cancelButton}
|
||||
>
|
||||
<Text style={[styles.cancelButtonText, { color: currentTheme.colors.text }]}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
</FocusableTouchableOpacity>
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => setLanguagePickerVisible(false)}
|
||||
style={[styles.doneButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
>
|
||||
<Text style={[styles.doneButtonText, { color: currentTheme.colors.white }]}>Done</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Switch,
|
||||
ScrollView,
|
||||
Platform,
|
||||
|
|
@ -24,6 +23,7 @@ import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext';
|
|||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ const ThemeCard: React.FC<ThemeCardProps> = ({
|
|||
onDelete
|
||||
}) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.themeCard,
|
||||
isSelected && styles.selectedThemeCard,
|
||||
|
|
@ -85,24 +85,24 @@ const ThemeCard: React.FC<ThemeCardProps> = ({
|
|||
{theme.isEditable && (
|
||||
<View style={styles.themeCardActions}>
|
||||
{onEdit && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.themeCardAction, styles.buttonShadow]}
|
||||
onPress={onEdit}
|
||||
>
|
||||
<MaterialIcons name="edit" size={16} color={theme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
{onDelete && (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[styles.themeCardAction, styles.buttonShadow]}
|
||||
onPress={onDelete}
|
||||
>
|
||||
<MaterialIcons name="delete" size={16} color={theme.colors.error} />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ const FilterTab: React.FC<FilterTabProps> = ({
|
|||
onPress,
|
||||
primaryColor
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.filterTab,
|
||||
isActive && { backgroundColor: primaryColor },
|
||||
|
|
@ -136,7 +136,7 @@ const FilterTab: React.FC<FilterTabProps> = ({
|
|||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
);
|
||||
|
||||
type ColorKey = 'primary' | 'secondary' | 'darkBackground';
|
||||
|
|
@ -242,12 +242,12 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
|||
return (
|
||||
<View style={styles.editorContainer}>
|
||||
<View style={styles.editorHeader}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.editorBackButton}
|
||||
onPress={onCancel}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={20} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
<TextInput
|
||||
style={styles.editorTitleInput}
|
||||
value={themeName}
|
||||
|
|
@ -255,12 +255,12 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
|||
placeholder="Theme name"
|
||||
placeholderTextColor="rgba(255,255,255,0.5)"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.editorSaveButton}
|
||||
onPress={handleSave}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>Save</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.editorBody}>
|
||||
|
|
@ -268,7 +268,7 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
|||
<ThemePreview />
|
||||
|
||||
<View style={styles.colorButtonsColumn}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.colorSelectorButton,
|
||||
selectedColorKey === 'primary' && styles.selectedColorButton,
|
||||
|
|
@ -277,9 +277,9 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
|||
onPress={() => setSelectedColorKey('primary')}
|
||||
>
|
||||
<Text style={styles.colorButtonText}>Primary</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.colorSelectorButton,
|
||||
selectedColorKey === 'secondary' && styles.selectedColorButton,
|
||||
|
|
@ -288,9 +288,9 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
|||
onPress={() => setSelectedColorKey('secondary')}
|
||||
>
|
||||
<Text style={styles.colorButtonText}>Secondary</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.colorSelectorButton,
|
||||
selectedColorKey === 'darkBackground' && styles.selectedColorButton,
|
||||
|
|
@ -299,7 +299,7 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
|||
onPress={() => setSelectedColorKey('darkBackground')}
|
||||
>
|
||||
<Text style={styles.colorButtonText}>Background</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -535,7 +535,7 @@ const ThemeScreen: React.FC = () => {
|
|||
<StatusBar barStyle="light-content" />
|
||||
|
||||
<View style={[styles.header, { paddingTop: headerTopPadding }]}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
|
|
@ -543,7 +543,7 @@ const ThemeScreen: React.FC = () => {
|
|||
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
||||
Settings
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Empty for now, but ready for future actions */}
|
||||
|
|
@ -595,7 +595,7 @@ const ThemeScreen: React.FC = () => {
|
|||
))}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.createButton,
|
||||
{ backgroundColor: currentTheme.colors.primary },
|
||||
|
|
@ -605,7 +605,7 @@ const ThemeScreen: React.FC = () => {
|
|||
>
|
||||
<MaterialIcons name="add" size={20} color="#FFFFFF" />
|
||||
<Text style={styles.createButtonText}>Create Custom Theme</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.textMuted, marginTop: 24 }]}>
|
||||
OPTIONS
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import {
|
|||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
|
|
@ -24,6 +23,7 @@ import { useTraktIntegration } from '../hooks/useTraktIntegration';
|
|||
import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings';
|
||||
import { colors } from '../styles';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||
|
||||
// Check if running on TV platform
|
||||
const isTV = Platform.isTV;
|
||||
|
|
@ -246,7 +246,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
]}>
|
||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
onPress={() => navigation.goBack()}
|
||||
style={styles.backButton}
|
||||
>
|
||||
|
|
@ -258,7 +258,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
<Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
|
||||
Settings
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
{/* Empty for now, but ready for future actions */}
|
||||
|
|
@ -328,7 +328,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
styles.signOutButton,
|
||||
|
|
@ -337,7 +337,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
onPress={handleSignOut}
|
||||
>
|
||||
<Text style={styles.buttonText}>Sign Out</Text>
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.signInContainer}>
|
||||
|
|
@ -358,7 +358,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
]}>
|
||||
Sync your watch history, watchlist, and collection with Trakt.tv
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
{ backgroundColor: isDarkMode ? currentTheme.colors.primary : currentTheme.colors.primary }
|
||||
|
|
@ -373,7 +373,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
Sign In with Trakt
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -448,7 +448,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
<FocusableTouchableOpacity
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
|
|
@ -478,7 +478,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
Sync Now
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</FocusableTouchableOpacity>
|
||||
|
||||
{/* Display Settings Section */}
|
||||
<Text style={[
|
||||
|
|
|
|||
21
src/styles/tvFocus.ts
Normal file
21
src/styles/tvFocus.ts
Normal 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 },
|
||||
};
|
||||
|
||||
Loading…
Reference in a new issue