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,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Pressable,
|
Pressable,
|
||||||
TouchableOpacity,
|
|
||||||
useColorScheme,
|
useColorScheme,
|
||||||
Platform,
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
@ -16,6 +15,7 @@ import Animated, {
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { Portal } from 'react-native-paper';
|
import { Portal } from 'react-native-paper';
|
||||||
|
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface CustomAlertProps {
|
interface CustomAlertProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
@ -120,7 +120,7 @@ export const CustomAlert = ({
|
||||||
{actions.map((action, idx) => {
|
{actions.map((action, idx) => {
|
||||||
const isPrimary = idx === actions.length - 1;
|
const isPrimary = idx === actions.length - 1;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={action.label}
|
key={action.label}
|
||||||
style={[
|
style={[
|
||||||
styles.actionButton,
|
styles.actionButton,
|
||||||
|
|
@ -132,6 +132,10 @@ export const CustomAlert = ({
|
||||||
]}
|
]}
|
||||||
onPress={() => handleActionPress(action)}
|
onPress={() => handleActionPress(action)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && visible && isPrimary}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.actionText,
|
styles.actionText,
|
||||||
|
|
@ -141,7 +145,7 @@ export const CustomAlert = ({
|
||||||
]}>
|
]}>
|
||||||
{action.label}
|
{action.label}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { memo, useCallback } from 'react';
|
import React, { memo, useCallback, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, FlatList } from 'react-native';
|
import { View, Text, StyleSheet, FlatList, Platform } from 'react-native';
|
||||||
|
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface ProviderFilterProps {
|
interface ProviderFilterProps {
|
||||||
selectedProvider: string;
|
selectedProvider: string;
|
||||||
|
|
@ -15,14 +16,24 @@ const ProviderFilter = memo(({
|
||||||
theme
|
theme
|
||||||
}: ProviderFilterProps) => {
|
}: ProviderFilterProps) => {
|
||||||
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
||||||
|
const listRef = useRef<FlatList<any> | null>(null);
|
||||||
|
|
||||||
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
|
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.filterChip,
|
styles.filterChip,
|
||||||
selectedProvider === item.id && styles.filterChipSelected
|
selectedProvider === item.id && styles.filterChipSelected
|
||||||
]}
|
]}
|
||||||
onPress={() => onSelect(item.id)}
|
onPress={() => onSelect(item.id)}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
onFocus={() => {
|
||||||
|
if (!Platform.isTV) return;
|
||||||
|
try {
|
||||||
|
listRef.current?.scrollToIndex({ index, animated: true, viewPosition: 0.5 });
|
||||||
|
} catch { }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.filterChipText,
|
styles.filterChipText,
|
||||||
|
|
@ -30,12 +41,13 @@ const ProviderFilter = memo(({
|
||||||
]}>
|
]}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
), [selectedProvider, onSelect, styles]);
|
), [selectedProvider, onSelect, styles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
ref={listRef}
|
||||||
data={providers}
|
data={providers}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Platform,
|
Platform,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
|
|
@ -16,6 +15,7 @@ import QualityBadge from './metadata/QualityBadge';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
import { useDownloads } from '../contexts/DownloadsContext';
|
import { useDownloads } from '../contexts/DownloadsContext';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
|
import { FocusableTouchableOpacity } from './common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface StreamCardProps {
|
interface StreamCardProps {
|
||||||
stream: Stream;
|
stream: Stream;
|
||||||
|
|
@ -177,16 +177,22 @@ const StreamCard = memo(({
|
||||||
|
|
||||||
const isDebrid = streamInfo.isDebrid;
|
const isDebrid = streamInfo.isDebrid;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.streamCard,
|
styles.streamCard,
|
||||||
isLoading && styles.streamCardLoading,
|
isLoading && styles.streamCardLoading,
|
||||||
isDebrid && styles.streamCardHighlighted
|
isDebrid && styles.streamCardHighlighted,
|
||||||
]}
|
]}
|
||||||
|
>
|
||||||
|
<FocusableTouchableOpacity
|
||||||
|
style={{ flex: 1 }}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={handleLongPress}
|
onLongPress={handleLongPress}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
>
|
>
|
||||||
{/* Scraper Logo */}
|
{/* Scraper Logo */}
|
||||||
{showLogos && scraperLogo && (
|
{showLogos && scraperLogo && (
|
||||||
|
|
@ -250,21 +256,23 @@ const StreamCard = memo(({
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
</FocusableTouchableOpacity>
|
||||||
{settings?.enableDownloads !== false && (
|
|
||||||
<TouchableOpacity
|
{settings?.enableDownloads !== false && (
|
||||||
style={[styles.streamAction, { marginLeft: 8, backgroundColor: theme.colors.elevation2 }]}
|
<View style={{ justifyContent: 'center', marginLeft: 8 }}>
|
||||||
|
<FocusableTouchableOpacity
|
||||||
|
style={[styles.streamAction, { backgroundColor: theme.colors.elevation2 }]}
|
||||||
onPress={handleDownload}
|
onPress={handleDownload}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={15}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons name="download" size={20} color={theme.colors.highEmphasis} />
|
||||||
name="download"
|
</FocusableTouchableOpacity>
|
||||||
size={20}
|
</View>
|
||||||
color={theme.colors.highEmphasis}
|
)}
|
||||||
/>
|
</View>
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
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,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Platform,
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Feather, MaterialIcons } from '@expo/vector-icons';
|
import { Feather, MaterialIcons } from '@expo/vector-icons';
|
||||||
|
import { FocusableTouchableOpacity } from './FocusableTouchableOpacity';
|
||||||
|
|
||||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
|
|
||||||
|
|
@ -131,17 +131,20 @@ const ScreenHeader: React.FC<ScreenHeaderProps> = ({
|
||||||
>
|
>
|
||||||
<View style={styles.headerContent}>
|
<View style={styles.headerContent}>
|
||||||
{showBackButton ? (
|
{showBackButton ? (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={onBackPress}
|
onPress={onBackPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={999}
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name={backIconName as any}
|
name={backIconName as any}
|
||||||
size={24}
|
size={24}
|
||||||
color={currentTheme.colors.text}
|
color={currentTheme.colors.text}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{titleComponent ? (
|
{titleComponent ? (
|
||||||
|
|
@ -164,17 +167,20 @@ const ScreenHeader: React.FC<ScreenHeaderProps> = ({
|
||||||
{rightActionComponent ? (
|
{rightActionComponent ? (
|
||||||
<View style={styles.rightActionContainer}>{rightActionComponent}</View>
|
<View style={styles.rightActionContainer}>{rightActionComponent}</View>
|
||||||
) : rightActionIcon && onRightActionPress ? (
|
) : rightActionIcon && onRightActionPress ? (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.rightActionButton}
|
style={styles.rightActionButton}
|
||||||
onPress={onRightActionPress}
|
onPress={onRightActionPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={999}
|
||||||
>
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
name={rightActionIcon as any}
|
name={rightActionIcon as any}
|
||||||
size={24}
|
size={24}
|
||||||
color={currentTheme.colors.text}
|
color={currentTheme.colors.text}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.rightActionPlaceholder} />
|
<View style={styles.rightActionPlaceholder} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import { useTraktContext } from '../../contexts/TraktContext';
|
||||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||||
import { useWatchProgress } from '../../hooks/useWatchProgress';
|
import { useWatchProgress } from '../../hooks/useWatchProgress';
|
||||||
import { streamCacheService } from '../../services/streamCacheService';
|
import { streamCacheService } from '../../services/streamCacheService';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface AppleTVHeroProps {
|
interface AppleTVHeroProps {
|
||||||
featuredContent: StreamingContent | null;
|
featuredContent: StreamingContent | null;
|
||||||
|
|
@ -1177,7 +1178,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
style={logoAnimatedStyle}
|
style={logoAnimatedStyle}
|
||||||
>
|
>
|
||||||
{currentItem.logo && !logoError[currentIndex] ? (
|
{currentItem.logo && !logoError[currentIndex] ? (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (currentItem) {
|
if (currentItem) {
|
||||||
|
|
@ -1188,6 +1189,10 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={16}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.03}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -1211,9 +1216,9 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (currentItem) {
|
if (currentItem) {
|
||||||
|
|
@ -1224,13 +1229,17 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={16}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.03}
|
||||||
>
|
>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={styles.title} numberOfLines={2}>
|
<Text style={styles.title} numberOfLines={2}>
|
||||||
{currentItem.name}
|
{currentItem.name}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
|
@ -1253,12 +1262,16 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
{/* Action Buttons - Play and Save buttons */}
|
{/* Action Buttons - Play and Save buttons */}
|
||||||
<View style={styles.buttonsContainer}>
|
<View style={styles.buttonsContainer}>
|
||||||
{/* Play Button */}
|
{/* Play Button */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.playButton]}
|
style={[styles.playButton]}
|
||||||
onPress={handlePlayAction}
|
onPress={handlePlayAction}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
hasTVPreferredFocus={Platform.isTV}
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={18}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.04}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={playButtonText === 'Resume' ? "replay" : "play-arrow"}
|
name={playButtonText === 'Resume' ? "replay" : "play-arrow"}
|
||||||
|
|
@ -1266,21 +1279,25 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
||||||
color="#000"
|
color="#000"
|
||||||
/>
|
/>
|
||||||
<Text style={styles.playButtonText}>{playButtonText}</Text>
|
<Text style={styles.playButtonText}>{playButtonText}</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{/* Save Button */}
|
{/* Save Button */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.saveButton}
|
style={styles.saveButton}
|
||||||
onPress={handleSaveAction}
|
onPress={handleSaveAction}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={18}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.04}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={inLibrary ? "bookmark" : "bookmark-outline"}
|
name={inLibrary ? "bookmark" : "bookmark-outline"}
|
||||||
size={24}
|
size={24}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Pagination Dots */}
|
{/* Pagination Dots */}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { storageService } from '../../services/storageService';
|
||||||
import { TraktService } from '../../services/traktService';
|
import { TraktService } from '../../services/traktService';
|
||||||
import { useTraktContext } from '../../contexts/TraktContext';
|
import { useTraktContext } from '../../contexts/TraktContext';
|
||||||
import Animated, { FadeIn } from 'react-native-reanimated';
|
import Animated, { FadeIn } from 'react-native-reanimated';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface ContentItemProps {
|
interface ContentItemProps {
|
||||||
item: StreamingContent;
|
item: StreamingContent;
|
||||||
|
|
@ -302,12 +303,18 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Animated.View style={[styles.itemContainer, { width: finalWidth }]} entering={FadeIn.duration(300)}>
|
<Animated.View style={[styles.itemContainer, { width: finalWidth }]} entering={FadeIn.duration(300)}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.contentItem, { width: finalWidth, aspectRatio: finalAspectRatio, borderRadius }]}
|
style={[styles.contentItem, { width: finalWidth, aspectRatio: finalAspectRatio, borderRadius }]}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
onLongPress={handleLongPress}
|
onLongPress={handleLongPress}
|
||||||
delayLongPress={300}
|
delayLongPress={300}
|
||||||
|
// TV focus highlight: visible focus ring + scale, no dim-on-press
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={borderRadius}
|
||||||
|
focusRingColor={currentTheme.colors.primary}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={getDeviceType(width) === 'tv' ? 1.08 : 1.06}
|
||||||
>
|
>
|
||||||
<View ref={itemRef} style={[styles.contentItemContainer, { borderRadius }]}>
|
<View ref={itemRef} style={[styles.contentItemContainer, { borderRadius }]}>
|
||||||
{/* Image with FastImage for aggressive caching */}
|
{/* Image with FastImage for aggressive caching */}
|
||||||
|
|
@ -362,7 +369,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{settings.showPosterTitles && (
|
{settings.showPosterTitles && (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { stremioService } from '../../services/stremioService';
|
||||||
import { streamCacheService } from '../../services/streamCacheService';
|
import { streamCacheService } from '../../services/streamCacheService';
|
||||||
import { useSettings } from '../../hooks/useSettings';
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
import CustomAlert from '../../components/CustomAlert';
|
import CustomAlert from '../../components/CustomAlert';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Define interface for continue watching items
|
// Define interface for continue watching items
|
||||||
interface ContinueWatchingItem extends StreamingContent {
|
interface ContinueWatchingItem extends StreamingContent {
|
||||||
|
|
@ -1081,7 +1082,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
|
|
||||||
// Memoized render function for continue watching items
|
// Memoized render function for continue watching items
|
||||||
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
|
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.wideContentItem,
|
styles.wideContentItem,
|
||||||
{
|
{
|
||||||
|
|
@ -1096,6 +1097,11 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
onPress={() => handleContentPress(item)}
|
onPress={() => handleContentPress(item)}
|
||||||
onLongPress={() => handleLongPress(item)}
|
onLongPress={() => handleLongPress(item)}
|
||||||
delayLongPress={800}
|
delayLongPress={800}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={14}
|
||||||
|
focusRingColor={currentTheme.colors.primary}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={isTV ? 1.06 : 1.04}
|
||||||
>
|
>
|
||||||
{/* Poster Image */}
|
{/* Poster Image */}
|
||||||
<View style={[
|
<View style={[
|
||||||
|
|
@ -1242,7 +1248,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet]);
|
), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet]);
|
||||||
|
|
||||||
// Memoized key extractor
|
// Memoized key extractor
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Modal,
|
Modal,
|
||||||
Pressable,
|
Pressable,
|
||||||
TouchableOpacity,
|
|
||||||
useColorScheme,
|
useColorScheme,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Platform
|
Platform
|
||||||
|
|
@ -28,6 +27,7 @@ import {
|
||||||
GestureHandlerRootView,
|
GestureHandlerRootView,
|
||||||
} from 'react-native-gesture-handler';
|
} from 'react-native-gesture-handler';
|
||||||
import { StreamingContent } from '../../services/catalogService';
|
import { StreamingContent } from '../../services/catalogService';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface DropUpMenuProps {
|
interface DropUpMenuProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
@ -184,7 +184,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.menuOptions}>
|
<View style={styles.menuOptions}>
|
||||||
{menuOptions.map((option, index) => (
|
{menuOptions.map((option, index) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={option.action}
|
key={option.action}
|
||||||
style={[
|
style={[
|
||||||
styles.menuOption,
|
styles.menuOption,
|
||||||
|
|
@ -195,6 +195,10 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
||||||
onOptionSelect(option.action);
|
onOptionSelect(option.action);
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && visible && index === 0}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
|
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
|
||||||
|
|
@ -207,7 +211,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
||||||
]}>
|
]}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { SkeletonFeatured } from './SkeletonLoaders';
|
||||||
import { hasValidLogoFormat, isTmdbUrl } from '../../utils/logoUtils';
|
import { hasValidLogoFormat, isTmdbUrl } from '../../utils/logoUtils';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface FeaturedContentProps {
|
interface FeaturedContentProps {
|
||||||
featuredContent: StreamingContent | null;
|
featuredContent: StreamingContent | null;
|
||||||
|
|
@ -495,7 +496,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Animated.View style={[styles.tabletButtons as ViewStyle, buttonsAnimatedStyle]}>
|
<Animated.View style={[styles.tabletButtons as ViewStyle, buttonsAnimatedStyle]}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.tabletPlayButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
style={[styles.tabletPlayButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (featuredContent) {
|
if (featuredContent) {
|
||||||
|
|
@ -507,12 +508,17 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
hasTVPreferredFocus={Platform.isTV}
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={30}
|
||||||
|
focusRingColor={currentTheme.colors.primary}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.04}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="play-arrow" size={28} color={currentTheme.colors.black} />
|
<MaterialIcons name="play-arrow" size={28} color={currentTheme.colors.black} />
|
||||||
<Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
<Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||||
Play Now
|
Play Now
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.tabletSecondaryButton as ViewStyle, { backgroundColor: 'rgba(255,255,255,0.2)', borderColor: 'rgba(255,255,255,0.3)' }]}
|
style={[styles.tabletSecondaryButton as ViewStyle, { backgroundColor: 'rgba(255,255,255,0.2)', borderColor: 'rgba(255,255,255,0.3)' }]}
|
||||||
|
|
@ -631,7 +637,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (featuredContent) {
|
if (featuredContent) {
|
||||||
|
|
@ -643,12 +649,17 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
hasTVPreferredFocus={Platform.isTV}
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={30}
|
||||||
|
focusRingColor={currentTheme.colors.primary}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.04}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
||||||
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||||
Play
|
Play
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.infoButton as ViewStyle}
|
style={styles.infoButton as ViewStyle}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ import { TMDBService } from '../../services/tmdbService';
|
||||||
import TrailerService from '../../services/trailerService';
|
import TrailerService from '../../services/trailerService';
|
||||||
import TrailerPlayer from '../video/TrailerPlayer';
|
import TrailerPlayer from '../video/TrailerPlayer';
|
||||||
import { HERO_HEIGHT, SCREEN_WIDTH as width, IS_TABLET as isTablet } from '../../constants/dimensions';
|
import { HERO_HEIGHT, SCREEN_WIDTH as width, IS_TABLET as isTablet } from '../../constants/dimensions';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { height } = Dimensions.get('window');
|
const { height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -344,7 +345,7 @@ const ActionButtons = memo(({
|
||||||
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
<Animated.View style={[isTablet ? styles.tabletActionButtons : styles.actionButtons, animatedStyle]}>
|
||||||
{/* Single Row Layout - Play, Save, and optionally Collection/Ratings */}
|
{/* Single Row Layout - Play, Save, and optionally Collection/Ratings */}
|
||||||
<View style={styles.singleRowLayout}>
|
<View style={styles.singleRowLayout}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
playButtonStyle,
|
playButtonStyle,
|
||||||
isTablet && styles.tabletPlayButton,
|
isTablet && styles.tabletPlayButton,
|
||||||
|
|
@ -354,6 +355,10 @@ const ActionButtons = memo(({
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
hasTVPreferredFocus={Platform.isTV}
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTablet ? 20 : 18}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.04}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={(() => {
|
name={(() => {
|
||||||
|
|
@ -366,9 +371,9 @@ const ActionButtons = memo(({
|
||||||
color={isWatched && type === 'movie' ? "#fff" : "#000"}
|
color={isWatched && type === 'movie' ? "#fff" : "#000"}
|
||||||
/>
|
/>
|
||||||
<Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text>
|
<Text style={[playButtonTextStyle, isTablet && styles.tabletPlayButtonText]}>{finalPlayButtonText}</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.actionButton,
|
styles.actionButton,
|
||||||
styles.infoButton,
|
styles.infoButton,
|
||||||
|
|
@ -378,6 +383,10 @@ const ActionButtons = memo(({
|
||||||
onPress={handleSaveAction}
|
onPress={handleSaveAction}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTablet ? 20 : 18}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.04}
|
||||||
>
|
>
|
||||||
{Platform.OS === 'ios' ? (
|
{Platform.OS === 'ios' ? (
|
||||||
GlassViewComp && liquidGlassAvailable ? (
|
GlassViewComp && liquidGlassAvailable ? (
|
||||||
|
|
@ -399,15 +408,19 @@ const ActionButtons = memo(({
|
||||||
<Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}>
|
<Text style={[styles.infoButtonText, isTablet && styles.tabletInfoButtonText]}>
|
||||||
{inLibrary ? 'Saved' : 'Save'}
|
{inLibrary ? 'Saved' : 'Save'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{/* Trakt Collection Button */}
|
{/* Trakt Collection Button */}
|
||||||
{hasTraktCollection && (
|
{hasTraktCollection && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
||||||
onPress={handleCollectionAction}
|
onPress={handleCollectionAction}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={999}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.06}
|
||||||
>
|
>
|
||||||
{Platform.OS === 'ios' ? (
|
{Platform.OS === 'ios' ? (
|
||||||
GlassViewComp && liquidGlassAvailable ? (
|
GlassViewComp && liquidGlassAvailable ? (
|
||||||
|
|
@ -426,16 +439,20 @@ const ActionButtons = memo(({
|
||||||
size={isTablet ? 28 : 24}
|
size={isTablet ? 28 : 24}
|
||||||
color={isInCollection ? "#3498DB" : currentTheme.colors.white}
|
color={isInCollection ? "#3498DB" : currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Ratings Button (for series) */}
|
{/* Ratings Button (for series) */}
|
||||||
{hasRatings && (
|
{hasRatings && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
style={[styles.iconButton, isTablet && styles.tabletIconButton, styles.singleRowIconButton]}
|
||||||
onPress={handleRatingsPress}
|
onPress={handleRatingsPress}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={999}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.06}
|
||||||
>
|
>
|
||||||
{Platform.OS === 'ios' ? (
|
{Platform.OS === 'ios' ? (
|
||||||
GlassViewComp && liquidGlassAvailable ? (
|
GlassViewComp && liquidGlassAvailable ? (
|
||||||
|
|
@ -454,7 +471,7 @@ const ActionButtons = memo(({
|
||||||
size={isTablet ? 28 : 24}
|
size={isTablet ? 28 : 24}
|
||||||
color={currentTheme.colors.white}
|
color={currentTheme.colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -1757,7 +1774,7 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
right: width >= 768 ? 32 : 16,
|
right: width >= 768 ? 32 : 16,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}>
|
}}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// Extract episode info if it's a series
|
// Extract episode info if it's a series
|
||||||
let episodeData = null;
|
let episodeData = null;
|
||||||
|
|
@ -1782,6 +1799,10 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={20}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.06}
|
||||||
style={{
|
style={{
|
||||||
padding: 8,
|
padding: 8,
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
|
@ -1793,19 +1814,27 @@ const HeroSection: React.FC<HeroSectionProps> = memo(({
|
||||||
size={24}
|
size={24}
|
||||||
color="white"
|
color="white"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Animated.View style={styles.backButtonContainer}>
|
<Animated.View style={styles.backButtonContainer}>
|
||||||
<TouchableOpacity style={styles.backButton} onPress={handleBack} focusable={Platform.isTV}>
|
<FocusableTouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={handleBack}
|
||||||
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={999}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.06}
|
||||||
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="arrow-back"
|
name="arrow-back"
|
||||||
size={28}
|
size={28}
|
||||||
color="#fff"
|
color="#fff"
|
||||||
style={styles.backButtonIcon}
|
style={styles.backButtonIcon}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* Ultra-light Gradient with subtle dynamic background blend */}
|
{/* Ultra-light Gradient with subtle dynamic background blend */}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { TMDBService } from '../../services/tmdbService';
|
import { TMDBService } from '../../services/tmdbService';
|
||||||
import { catalogService } from '../../services/catalogService';
|
import { catalogService } from '../../services/catalogService';
|
||||||
import CustomAlert from '../../components/CustomAlert';
|
import CustomAlert from '../../components/CustomAlert';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -117,9 +118,15 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: StreamingContent }) => (
|
const renderItem = ({ item }: { item: StreamingContent }) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.itemContainer, { width: posterWidth, marginRight: itemSpacing }]}
|
style={[styles.itemContainer, { width: posterWidth, marginRight: itemSpacing }]}
|
||||||
onPress={() => handleItemPress(item)}
|
onPress={() => handleItemPress(item)}
|
||||||
|
activeOpacity={0.9}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTV ? 12 : isLargeTablet ? 10 : isTablet ? 10 : 8}
|
||||||
|
focusRingColor={currentTheme.colors.primary}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={isTV ? 1.06 : 1.04}
|
||||||
>
|
>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: item.poster }}
|
source={{ uri: item.poster }}
|
||||||
|
|
@ -129,7 +136,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
||||||
<Text style={[styles.title, { color: currentTheme.colors.mediumEmphasis, fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13, lineHeight: isTV ? 20 : 18 }]} numberOfLines={2}>
|
<Text style={[styles.title, { color: currentTheme.colors.mediumEmphasis, fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13, lineHeight: isTV ? 20 : 18 }]} numberOfLines={2}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loadingRecommendations) {
|
if (loadingRecommendations) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { TraktService } from '../../services/traktService';
|
||||||
import { watchedService } from '../../services/watchedService';
|
import { watchedService } from '../../services/watchedService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { mmkvStorage } from '../../services/mmkvStorage';
|
import { mmkvStorage } from '../../services/mmkvStorage';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Enhanced responsive breakpoints for Seasons Section
|
// Enhanced responsive breakpoints for Seasons Section
|
||||||
const BREAKPOINTS = {
|
const BREAKPOINTS = {
|
||||||
|
|
@ -779,7 +780,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
]}>Seasons</Text>
|
]}>Seasons</Text>
|
||||||
|
|
||||||
{/* Dropdown Toggle Button */}
|
{/* Dropdown Toggle Button */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.seasonViewToggle,
|
styles.seasonViewToggle,
|
||||||
{
|
{
|
||||||
|
|
@ -796,6 +797,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
]}
|
]}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTV ? 10 : isLargeTablet ? 8 : isTablet ? 6 : 6}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.03}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.seasonViewToggleText,
|
styles.seasonViewToggleText,
|
||||||
|
|
@ -808,7 +813,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
]}>
|
]}>
|
||||||
{seasonViewMode === 'posters' ? 'Posters' : 'Text'}
|
{seasonViewMode === 'posters' ? 'Posters' : 'Text'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
@ -846,7 +851,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
key={season}
|
key={season}
|
||||||
style={{ opacity: textViewVisible ? 1 : 0 }}
|
style={{ opacity: textViewVisible ? 1 : 0 }}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.seasonTextButton,
|
styles.seasonTextButton,
|
||||||
{
|
{
|
||||||
|
|
@ -860,6 +865,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
]}
|
]}
|
||||||
onPress={() => onSeasonChange(season)}
|
onPress={() => onSeasonChange(season)}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.03}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.seasonTextButtonText,
|
styles.seasonTextButtonText,
|
||||||
|
|
@ -873,7 +882,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
]} numberOfLines={1}>
|
]} numberOfLines={1}>
|
||||||
{season === 0 ? 'Specials' : `Season ${season}`}
|
{season === 0 ? 'Specials' : `Season ${season}`}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -885,7 +894,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
key={season}
|
key={season}
|
||||||
style={{ opacity: posterViewVisible ? 1 : 0 }}
|
style={{ opacity: posterViewVisible ? 1 : 0 }}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.seasonButton,
|
styles.seasonButton,
|
||||||
{
|
{
|
||||||
|
|
@ -896,6 +905,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
]}
|
]}
|
||||||
onPress={() => onSeasonChange(season)}
|
onPress={() => onSeasonChange(season)}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.03}
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.seasonPosterContainer,
|
styles.seasonPosterContainer,
|
||||||
|
|
@ -937,7 +950,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
>
|
>
|
||||||
{season === 0 ? 'Specials' : `Season ${season}`}
|
{season === 0 ? 'Specials' : `Season ${season}`}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
@ -1022,7 +1035,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
const showProgress = progress && progressPercent < 85;
|
const showProgress = progress && progressPercent < 85;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={episode.id}
|
key={episode.id}
|
||||||
style={[
|
style={[
|
||||||
styles.episodeCardVertical,
|
styles.episodeCardVertical,
|
||||||
|
|
@ -1038,6 +1051,10 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
delayLongPress={400}
|
delayLongPress={400}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.02}
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.episodeImageContainer,
|
styles.episodeImageContainer,
|
||||||
|
|
@ -1228,7 +1245,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
|
||||||
{(episode.overview || (episode as any).description || (episode as any).plot || (episode as any).synopsis || 'No description available')}
|
{(episode.overview || (episode as any).description || (episode as any).plot || (episode as any).synopsis || 'No description available')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { logger } from '../../utils/logger';
|
||||||
import TrailerService from '../../services/trailerService';
|
import TrailerService from '../../services/trailerService';
|
||||||
import TrailerModal from './TrailerModal';
|
import TrailerModal from './TrailerModal';
|
||||||
import Animated, { useSharedValue, withTiming, withDelay, useAnimatedStyle } from 'react-native-reanimated';
|
import Animated, { useSharedValue, withTiming, withDelay, useAnimatedStyle } from 'react-native-reanimated';
|
||||||
|
import { FocusableTouchableOpacity } from '../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Enhanced responsive breakpoints for Trailers Section
|
// Enhanced responsive breakpoints for Trailers Section
|
||||||
const BREAKPOINTS = {
|
const BREAKPOINTS = {
|
||||||
|
|
@ -517,7 +518,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
|
|
||||||
{/* Category Selector - Right Aligned */}
|
{/* Category Selector - Right Aligned */}
|
||||||
{trailerCategories.length > 0 && selectedCategory && (
|
{trailerCategories.length > 0 && selectedCategory && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.categorySelector,
|
styles.categorySelector,
|
||||||
{
|
{
|
||||||
|
|
@ -531,6 +532,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
onPress={toggleDropdown}
|
onPress={toggleDropdown}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.03}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -551,7 +556,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
size={isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18}
|
size={isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18}
|
||||||
color="rgba(255,255,255,0.7)"
|
color="rgba(255,255,255,0.7)"
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -575,7 +580,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
|
||||||
}]}>
|
}]}>
|
||||||
{trailerCategories.map(category => (
|
{trailerCategories.map(category => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={category}
|
key={category}
|
||||||
style={[
|
style={[
|
||||||
styles.dropdownItem,
|
styles.dropdownItem,
|
||||||
|
|
@ -587,6 +592,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
onPress={() => handleCategorySelect(category)}
|
onPress={() => handleCategorySelect(category)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTV ? 18 : isLargeTablet ? 16 : isTablet ? 14 : 14}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.02}
|
||||||
>
|
>
|
||||||
<View style={styles.dropdownItemContent}>
|
<View style={styles.dropdownItemContent}>
|
||||||
<View style={[
|
<View style={[
|
||||||
|
|
@ -626,7 +635,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
{trailers[category].length}
|
{trailers[category].length}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
@ -656,7 +665,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
{ width: trailerCardWidth }
|
{ width: trailerCardWidth }
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.trailerCard,
|
styles.trailerCard,
|
||||||
{
|
{
|
||||||
|
|
@ -667,6 +676,10 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
onPress={() => handleTrailerPress(trailer)}
|
onPress={() => handleTrailerPress(trailer)}
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
focusBorderRadius={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16}
|
||||||
|
focusRingWidth={3}
|
||||||
|
focusScale={1.03}
|
||||||
>
|
>
|
||||||
{/* Thumbnail with Gradient Overlay */}
|
{/* Thumbnail with Gradient Overlay */}
|
||||||
<View style={styles.thumbnailWrapper}>
|
<View style={styles.thumbnailWrapper}>
|
||||||
|
|
@ -688,7 +701,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
||||||
}
|
}
|
||||||
]} />
|
]} />
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{/* Trailer Info Below Card */}
|
{/* Trailer Info Below Card */}
|
||||||
<View style={styles.trailerInfoBelow}>
|
<View style={styles.trailerInfoBelow}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native';
|
import { View, Text, Pressable, ScrollView, useWindowDimensions, StyleSheet, Platform } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
|
|
@ -9,6 +9,7 @@ import Animated, {
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
|
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface AudioTrackModalProps {
|
interface AudioTrackModalProps {
|
||||||
showAudioModal: boolean;
|
showAudioModal: boolean;
|
||||||
|
|
@ -38,17 +39,17 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
return (
|
return (
|
||||||
<View style={StyleSheet.absoluteFill} zIndex={9999}>
|
<View style={StyleSheet.absoluteFill} zIndex={9999}>
|
||||||
{/* Backdrop matching SubtitleModal */}
|
{/* Backdrop matching SubtitleModal */}
|
||||||
<TouchableOpacity
|
<Pressable
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
activeOpacity={1}
|
|
||||||
onPress={handleClose}
|
onPress={handleClose}
|
||||||
|
focusable={false}
|
||||||
>
|
>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeIn.duration(200)}
|
entering={FadeIn.duration(200)}
|
||||||
exiting={FadeOut.duration(150)}
|
exiting={FadeOut.duration(150)}
|
||||||
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }}
|
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.4)' }}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
|
|
||||||
{/* Center Alignment Container */}
|
{/* Center Alignment Container */}
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none">
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none">
|
||||||
|
|
@ -79,7 +80,7 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
const isSelected = selectedAudioTrack === track.id;
|
const isSelected = selectedAudioTrack === track.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={track.id}
|
key={track.id}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
selectAudioTrack(track.id);
|
selectAudioTrack(track.id);
|
||||||
|
|
@ -93,6 +94,10 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
}}
|
}}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && isSelected}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
|
|
@ -104,7 +109,7 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{isSelected && <MaterialIcons name="check" size={18} color="black" />}
|
{isSelected && <MaterialIcons name="check" size={18} color="black" />}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native';
|
import { View, Text, Pressable, ScrollView, ActivityIndicator, StyleSheet, Platform, useWindowDimensions } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
|
|
@ -11,6 +11,7 @@ import { Episode } from '../../../types/metadata';
|
||||||
import { Stream } from '../../../types/streams';
|
import { Stream } from '../../../types/streams';
|
||||||
import { stremioService } from '../../../services/stremioService';
|
import { stremioService } from '../../../services/stremioService';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface EpisodeStreamsModalProps {
|
interface EpisodeStreamsModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
@ -142,17 +143,17 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
|
||||||
return (
|
return (
|
||||||
<View style={StyleSheet.absoluteFill} zIndex={10000}>
|
<View style={StyleSheet.absoluteFill} zIndex={10000}>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<TouchableOpacity
|
<Pressable
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
activeOpacity={1}
|
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
|
focusable={false}
|
||||||
>
|
>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeIn.duration(200)}
|
entering={FadeIn.duration(200)}
|
||||||
exiting={FadeOut.duration(150)}
|
exiting={FadeOut.duration(150)}
|
||||||
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }}
|
style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={SlideInRight.duration(300)}
|
entering={SlideInRight.duration(300)}
|
||||||
|
|
@ -218,7 +219,7 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
|
||||||
const quality = getQualityFromTitle(stream.title) || stream.quality;
|
const quality = getQualityFromTitle(stream.title) || stream.quality;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={`${providerId}-${index}`}
|
key={`${providerId}-${index}`}
|
||||||
style={{
|
style={{
|
||||||
padding: 8,
|
padding: 8,
|
||||||
|
|
@ -232,6 +233,10 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && index === 0}
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
|
|
@ -248,7 +253,7 @@ export const EpisodeStreamsModal: React.FC<EpisodeStreamsModalProps> = ({
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView, useWindowDimensions, StyleSheet, Platform, ActivityIndicator } from 'react-native';
|
import { View, Text, Pressable, ScrollView, useWindowDimensions, StyleSheet, Platform, ActivityIndicator } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
|
|
@ -12,6 +12,7 @@ import { EpisodeCard } from '../cards/EpisodeCard';
|
||||||
import { storageService } from '../../../services/storageService';
|
import { storageService } from '../../../services/storageService';
|
||||||
import { TraktService } from '../../../services/traktService';
|
import { TraktService } from '../../../services/traktService';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface EpisodesModalProps {
|
interface EpisodesModalProps {
|
||||||
showEpisodesModal: boolean;
|
showEpisodesModal: boolean;
|
||||||
|
|
@ -97,9 +98,9 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
|
<View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
|
||||||
<TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={() => setShowEpisodesModal(false)}>
|
<Pressable style={StyleSheet.absoluteFill} onPress={() => setShowEpisodesModal(false)} focusable={false}>
|
||||||
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
|
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={SlideInRight.duration(300)}
|
entering={SlideInRight.duration(300)}
|
||||||
|
|
@ -127,7 +128,7 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
|
||||||
if (b === 0) return -1;
|
if (b === 0) return -1;
|
||||||
return a - b;
|
return a - b;
|
||||||
}).map((season) => (
|
}).map((season) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={season}
|
key={season}
|
||||||
onPress={() => setSelectedSeason(season)}
|
onPress={() => setSelectedSeason(season)}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -138,6 +139,10 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: selectedSeason === season ? 'white' : 'rgba(255,255,255,0.1)',
|
borderColor: selectedSeason === season ? 'white' : 'rgba(255,255,255,0.1)',
|
||||||
}}
|
}}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={20}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && selectedSeason === season}
|
||||||
>
|
>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
color: selectedSeason === season ? 'black' : 'white',
|
color: selectedSeason === season ? 'black' : 'white',
|
||||||
|
|
@ -145,7 +150,7 @@ export const EpisodesModal: React.FC<EpisodesModalProps> = ({
|
||||||
}}>
|
}}>
|
||||||
{season === 0 ? 'Specials' : `Season ${season}`}
|
{season === 0 ? 'Specials' : `Season ${season}`}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, StyleSheet, useWindowDimensions, Platform } from 'react-native';
|
import { View, Text, Pressable, StyleSheet, useWindowDimensions, Platform } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
|
|
@ -7,6 +7,7 @@ import Animated, {
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Check if running on TV platform
|
// Check if running on TV platform
|
||||||
const isTV = Platform.isTV;
|
const isTV = Platform.isTV;
|
||||||
|
|
@ -58,9 +59,9 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[StyleSheet.absoluteFill, { zIndex: 99999, justifyContent: 'center', alignItems: 'center' }]}>
|
<View style={[StyleSheet.absoluteFill, { zIndex: 99999, justifyContent: 'center', alignItems: 'center' }]}>
|
||||||
<TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleClose}>
|
<Pressable style={StyleSheet.absoluteFill} onPress={handleClose} focusable={false}>
|
||||||
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.7)' }} />
|
<Animated.View entering={FadeIn.duration(200)} exiting={FadeOut.duration(150)} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.7)' }} />
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeIn.duration(300)}
|
entering={FadeIn.duration(300)}
|
||||||
|
|
@ -111,29 +112,35 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
||||||
{errorDetails || 'An unknown error occurred during playback.'}
|
{errorDetails || 'An unknown error occurred during playback.'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<TouchableOpacity
|
{!!ExpoClipboard && (
|
||||||
onPress={handleCopy}
|
<FocusableTouchableOpacity
|
||||||
style={{
|
onPress={handleCopy}
|
||||||
flexDirection: 'row',
|
activeOpacity={0.9}
|
||||||
alignItems: 'center',
|
enableTVFocus={Platform.isTV}
|
||||||
justifyContent: 'center',
|
preset="pill"
|
||||||
padding: 8,
|
focusBorderRadius={12}
|
||||||
marginBottom: 24,
|
style={{
|
||||||
opacity: 0.8
|
flexDirection: 'row',
|
||||||
}}
|
alignItems: 'center',
|
||||||
>
|
justifyContent: 'center',
|
||||||
<MaterialIcons
|
padding: 8,
|
||||||
name={copied ? "check" : "content-copy"}
|
marginBottom: 24,
|
||||||
size={16}
|
opacity: 0.9
|
||||||
color="rgba(255,255,255,0.6)"
|
}}
|
||||||
style={{ marginRight: 6 }}
|
>
|
||||||
/>
|
<MaterialIcons
|
||||||
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontWeight: '500' }}>
|
name={copied ? "check" : "content-copy"}
|
||||||
{copied ? 'Copied to clipboard' : 'Copy error details'}
|
size={16}
|
||||||
</Text>
|
color="rgba(255,255,255,0.6)"
|
||||||
</TouchableOpacity>
|
style={{ marginRight: 6 }}
|
||||||
|
/>
|
||||||
|
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 13, fontWeight: '500' }}>
|
||||||
|
{copied ? 'Copied to clipboard' : 'Copy error details'}
|
||||||
|
</Text>
|
||||||
|
</FocusableTouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
|
|
@ -144,6 +151,10 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
||||||
}}
|
}}
|
||||||
onPress={handleClose}
|
onPress={handleClose}
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
color: 'black',
|
color: 'black',
|
||||||
|
|
@ -152,7 +163,7 @@ export const ErrorModal: React.FC<ErrorModalProps> = ({
|
||||||
}}>
|
}}>
|
||||||
Dismiss
|
Dismiss
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { View, TouchableOpacity, Animated, ActivityIndicator, StyleSheet, Image } from 'react-native';
|
import { View, Animated, ActivityIndicator, StyleSheet, Image, Platform } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import Reanimated, {
|
import Reanimated, {
|
||||||
|
|
@ -12,6 +12,7 @@ import Reanimated, {
|
||||||
withDelay
|
withDelay
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { styles } from '../utils/playerStyles';
|
import { styles } from '../utils/playerStyles';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface LoadingOverlayProps {
|
interface LoadingOverlayProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
@ -118,13 +119,17 @@ const LoadingOverlay: React.FC<LoadingOverlayProps> = ({
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.loadingCloseButton}
|
style={styles.loadingCloseButton}
|
||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={999}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="close" size={24} color="#ffffff" />
|
<MaterialIcons name="close" size={24} color="#ffffff" />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.openingContent}>
|
<View style={styles.openingContent}>
|
||||||
{hasLogo && logo ? (
|
{hasLogo && logo ? (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { View, Text, TouchableOpacity } from 'react-native';
|
import { View, Text, Platform } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { styles } from '../utils/playerStyles';
|
import { styles } from '../utils/playerStyles';
|
||||||
import { formatTime } from '../utils/playerUtils';
|
import { formatTime } from '../utils/playerUtils';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface ResumeOverlayProps {
|
interface ResumeOverlayProps {
|
||||||
showResumeOverlay: boolean;
|
showResumeOverlay: boolean;
|
||||||
|
|
@ -71,20 +72,27 @@ export const ResumeOverlay: React.FC<ResumeOverlayProps> = ({
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.resumeButtons}>
|
<View style={styles.resumeButtons}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.resumeButton}
|
style={styles.resumeButton}
|
||||||
onPress={handleStartFromBeginning}
|
onPress={handleStartFromBeginning}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={12}
|
||||||
>
|
>
|
||||||
<Ionicons name="refresh" size={16} color="white" style={styles.buttonIcon} />
|
<Ionicons name="refresh" size={16} color="white" style={styles.buttonIcon} />
|
||||||
<Text style={styles.resumeButtonText}>Start Over</Text>
|
<Text style={styles.resumeButtonText}>Start Over</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.resumeButton, styles.resumeFromButton]}
|
style={[styles.resumeButton, styles.resumeFromButton]}
|
||||||
onPress={handleResume}
|
onPress={handleResume}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Ionicons name="play" size={16} color="white" style={styles.buttonIcon} />
|
<Ionicons name="play" size={16} color="white" style={styles.buttonIcon} />
|
||||||
<Text style={styles.resumeButtonText}>Resume</Text>
|
<Text style={styles.resumeButtonText}>Resume</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import Animated, {
|
||||||
SlideOutRight,
|
SlideOutRight,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { Stream } from '../../../types/streams';
|
import { Stream } from '../../../types/streams';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface SourcesModalProps {
|
interface SourcesModalProps {
|
||||||
showSourcesModal: boolean;
|
showSourcesModal: boolean;
|
||||||
|
|
@ -168,7 +169,7 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
|
||||||
const quality = getQualityFromTitle(stream.title) || stream.quality;
|
const quality = getQualityFromTitle(stream.title) || stream.quality;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={`${providerId}-${index}`}
|
key={`${providerId}-${index}`}
|
||||||
style={{
|
style={{
|
||||||
padding: 8,
|
padding: 8,
|
||||||
|
|
@ -181,6 +182,10 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
|
||||||
onPress={() => handleStreamSelect(stream)}
|
onPress={() => handleStreamSelect(stream)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
disabled={isChangingSource === true}
|
disabled={isChangingSource === true}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && (isSelected || (providerId === sortedProviders[0]?.[0] && index === 0))}
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
|
|
@ -227,7 +232,7 @@ export const SourcesModal: React.FC<SourcesModalProps> = ({
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, useWindowDimensions, StyleSheet } from 'react-native';
|
import { View, Text, Pressable, useWindowDimensions, StyleSheet, Platform } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
|
|
@ -9,6 +9,7 @@ import Animated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
withTiming,
|
withTiming,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface SpeedModalProps {
|
interface SpeedModalProps {
|
||||||
showSpeedModal: boolean;
|
showSpeedModal: boolean;
|
||||||
|
|
@ -31,7 +32,15 @@ const MorphingButton = ({ label, isSelected, onPress, isSmall = false }: any) =>
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={onPress} activeOpacity={0.8} style={{ flex: isSmall ? 0 : 1 }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
style={{ flex: isSmall ? 0 : 1 }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={10}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && isSelected}
|
||||||
|
>
|
||||||
<Animated.View style={[{ paddingVertical: isSmall ? 6 : 8, paddingHorizontal: isSmall ? 14 : 0, alignItems: 'center', justifyContent: 'center' }, animatedStyle]}>
|
<Animated.View style={[{ paddingVertical: isSmall ? 6 : 8, paddingHorizontal: isSmall ? 14 : 0, alignItems: 'center', justifyContent: 'center' }, animatedStyle]}>
|
||||||
<Text style={{
|
<Text style={{
|
||||||
color: isSelected && !isSmall ? 'black' : 'white',
|
color: isSelected && !isSmall ? 'black' : 'white',
|
||||||
|
|
@ -41,7 +50,7 @@ const MorphingButton = ({ label, isSelected, onPress, isSmall = false }: any) =>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -62,14 +71,14 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
|
||||||
if (!showSpeedModal) return null;
|
if (!showSpeedModal) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={StyleSheet.absoluteFill} zIndex={9999}>
|
<View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
|
||||||
<TouchableOpacity
|
<Pressable
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
activeOpacity={1}
|
|
||||||
onPress={() => setShowSpeedModal(false)}
|
onPress={() => setShowSpeedModal(false)}
|
||||||
|
focusable={false}
|
||||||
>
|
>
|
||||||
<Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.2)' }} />
|
<Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.2)' }} />
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
|
|
||||||
<View pointerEvents="box-none" style={{ ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', paddingBottom: 20 }}>
|
<View pointerEvents="box-none" style={{ ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', paddingBottom: 20 }}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
|
@ -104,9 +113,13 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
|
||||||
|
|
||||||
{/* On Hold Section */}
|
{/* On Hold Section */}
|
||||||
<View>
|
<View>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => setHoldToSpeedEnabled(!holdToSpeedEnabled)}
|
onPress={() => setHoldToSpeedEnabled(!holdToSpeedEnabled)}
|
||||||
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: holdToSpeedEnabled ? 15 : 0 }}
|
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: holdToSpeedEnabled ? 15 : 0 }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14 }}>On Hold</Text>
|
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 14 }}>On Hold</Text>
|
||||||
<View style={{
|
<View style={{
|
||||||
|
|
@ -116,7 +129,7 @@ const SpeedModal: React.FC<SpeedModalProps> = ({
|
||||||
}}>
|
}}>
|
||||||
<View style={{ width: 14, height: 14, borderRadius: 7, backgroundColor: holdToSpeedEnabled ? 'black' : 'white' }} />
|
<View style={{ width: 14, height: 14, borderRadius: 7, backgroundColor: holdToSpeedEnabled ? 'black' : 'white' }} />
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{holdToSpeedEnabled && (
|
{holdToSpeedEnabled && (
|
||||||
<Animated.View entering={FadeIn} style={{ flexDirection: 'row', gap: 8 }}>
|
<Animated.View entering={FadeIn} style={{ flexDirection: 'row', gap: 8 }}>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView, Platform, useWindowDimensions, StyleSheet } from 'react-native';
|
import { View, Text, Pressable, ScrollView, Platform, useWindowDimensions, StyleSheet } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
|
|
@ -11,6 +11,7 @@ import Animated, {
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
|
import { WyzieSubtitle, SubtitleCue } from '../utils/playerTypes';
|
||||||
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
|
import { getTrackDisplayName, formatLanguage } from '../utils/playerUtils';
|
||||||
|
import { FocusableTouchableOpacity } from '../../common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface SubtitleModalsProps {
|
interface SubtitleModalsProps {
|
||||||
showSubtitleModal: boolean;
|
showSubtitleModal: boolean;
|
||||||
|
|
@ -65,13 +66,21 @@ const MorphingTab = ({ label, isSelected, onPress }: any) => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={onPress} activeOpacity={0.8} style={{ flex: 1 }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && isSelected}
|
||||||
|
>
|
||||||
<Animated.View style={[{ paddingVertical: 8, alignItems: 'center', justifyContent: 'center' }, animatedStyle]}>
|
<Animated.View style={[{ paddingVertical: 8, alignItems: 'center', justifyContent: 'center' }, animatedStyle]}>
|
||||||
<Text style={{ color: isSelected ? 'black' : 'white', fontWeight: isSelected ? '700' : '400', fontSize: 13 }}>
|
<Text style={{ color: isSelected ? 'black' : 'white', fontWeight: isSelected ? '700' : '400', fontSize: 13 }}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -112,11 +121,11 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
if (!showSubtitleModal) return null;
|
if (!showSubtitleModal) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={StyleSheet.absoluteFill} zIndex={9999}>
|
<View style={[StyleSheet.absoluteFill, { zIndex: 9999 }]}>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<TouchableOpacity style={StyleSheet.absoluteFill} activeOpacity={1} onPress={handleClose}>
|
<Pressable style={StyleSheet.absoluteFill} onPress={handleClose} focusable={false}>
|
||||||
<Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
|
<Animated.View entering={FadeIn} exiting={FadeOut} style={{ flex: 1, backgroundColor: 'rgba(0,0,0,0.5)' }} />
|
||||||
</TouchableOpacity>
|
</Pressable>
|
||||||
|
|
||||||
{/* Centered Modal Container */}
|
{/* Centered Modal Container */}
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none">
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} pointerEvents="box-none">
|
||||||
|
|
@ -149,21 +158,29 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
<View style={{ paddingHorizontal: 20, paddingBottom: 20 }}>
|
<View style={{ paddingHorizontal: 20, paddingBottom: 20 }}>
|
||||||
{activeTab === 'built-in' && (
|
{activeTab === 'built-in' && (
|
||||||
<View style={{ gap: 8 }}>
|
<View style={{ gap: 8 }}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }}
|
onPress={() => { selectTextTrack(-1); setSelectedOnlineSubtitleId(null); }}
|
||||||
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }}
|
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === -1 ? 'white' : 'rgba(242, 184, 181)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && selectedTextTrack === -1}
|
||||||
>
|
>
|
||||||
<Text style={{ color: selectedTextTrack === -1 ? 'black' : 'rgba(96, 20, 16)', fontWeight: '600' }}>None</Text>
|
<Text style={{ color: selectedTextTrack === -1 ? 'black' : 'rgba(96, 20, 16)', fontWeight: '600' }}>None</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{ksTextTracks.map((track) => (
|
{ksTextTracks.map((track) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={track.id}
|
key={track.id}
|
||||||
onPress={() => { selectTextTrack(track.id); setSelectedOnlineSubtitleId(null); }}
|
onPress={() => { selectTextTrack(track.id); setSelectedOnlineSubtitleId(null); }}
|
||||||
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === track.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
|
style={{ padding: 10, borderRadius: 12, backgroundColor: selectedTextTrack === track.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && selectedTextTrack === track.id}
|
||||||
>
|
>
|
||||||
<Text style={{ color: selectedTextTrack === track.id ? 'black' : 'white' }}>{getTrackDisplayName(track)}</Text>
|
<Text style={{ color: selectedTextTrack === track.id ? 'black' : 'white' }}>{getTrackDisplayName(track)}</Text>
|
||||||
{selectedTextTrack === track.id && <MaterialIcons name="check" size={18} color="black" />}
|
{selectedTextTrack === track.id && <MaterialIcons name="check" size={18} color="black" />}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -171,23 +188,34 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
{activeTab === 'addon' && (
|
{activeTab === 'addon' && (
|
||||||
<View style={{ gap: 8 }}>
|
<View style={{ gap: 8 }}>
|
||||||
{availableSubtitles.length === 0 ? (
|
{availableSubtitles.length === 0 ? (
|
||||||
<TouchableOpacity onPress={fetchAvailableSubtitles} style={{ padding: 40, alignItems: 'center', opacity: 0.5 }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={fetchAvailableSubtitles}
|
||||||
|
style={{ padding: 40, alignItems: 'center', opacity: 0.8 }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="card"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
|
>
|
||||||
<MaterialIcons name="cloud-download" size={32} color="white" />
|
<MaterialIcons name="cloud-download" size={32} color="white" />
|
||||||
<Text style={{ color: 'white', marginTop: 10 }}>Search Online Subtitles</Text>
|
<Text style={{ color: 'white', marginTop: 10 }}>Search Online Subtitles</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
) : (
|
) : (
|
||||||
availableSubtitles.map((sub) => (
|
availableSubtitles.map((sub) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={sub.id}
|
key={sub.id}
|
||||||
onPress={() => { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }}
|
onPress={() => { setSelectedOnlineSubtitleId(sub.id); loadWyzieSubtitle(sub); }}
|
||||||
style={{ padding: 5,paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', textAlignVertical: 'center' }}
|
style={{ padding: 5, paddingLeft: 8, paddingRight: 10, borderRadius: 12, backgroundColor: selectedOnlineSubtitleId === sub.id ? 'white' : 'rgba(255,255,255,0.05)', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && selectedOnlineSubtitleId === sub.id}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ marginLeft: 5, color: selectedOnlineSubtitleId === sub.id ? 'black' : 'white', fontWeight: '600' }}>{sub.display}</Text>
|
<Text style={{ marginLeft: 5, color: selectedOnlineSubtitleId === sub.id ? 'black' : 'white', fontWeight: '600' }}>{sub.display}</Text>
|
||||||
<Text style={{ marginLeft: 5, color: selectedOnlineSubtitleId === sub.id ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)', fontSize: 11, paddingBottom: 3 }}>{formatLanguage(sub.language)}</Text>
|
<Text style={{ marginLeft: 5, color: selectedOnlineSubtitleId === sub.id ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)', fontSize: 11, paddingBottom: 3 }}>{formatLanguage(sub.language)}</Text>
|
||||||
</View>
|
</View>
|
||||||
{selectedOnlineSubtitleId === sub.id && <MaterialIcons name="check" size={18} color="black" />}
|
{selectedOnlineSubtitleId === sub.id && <MaterialIcons name="check" size={18} color="black" />}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -233,7 +261,7 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>Quick Presets</Text>
|
<Text style={{ color: 'rgba(255,255,255,0.7)', fontSize: 12, marginLeft: 6, fontWeight: '600' }}>Quick Presets</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
|
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
|
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
|
||||||
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
|
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
|
||||||
|
|
@ -241,33 +269,45 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
setSubtitleLineHeightMultiplier(1.2);
|
setSubtitleLineHeightMultiplier(1.2);
|
||||||
}}
|
}}
|
||||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
|
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={20}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 11 : 12 }}>Default</Text>
|
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 11 : 12 }}>Default</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false);
|
setSubtitleTextColor('#FFD700'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4); setSubtitleBgOpacity(0.3); setSubtitleTextShadow(false);
|
||||||
}}
|
}}
|
||||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }}
|
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(255,215,0,0.12)', borderWidth: 1, borderColor: 'rgba(255,215,0,0.35)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={20}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#FFD700', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Yellow</Text>
|
<Text style={{ color: '#FFD700', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Yellow</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5);
|
setSubtitleTextColor('#FFFFFF'); setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(3); setSubtitleBgOpacity(0.0); setSubtitleTextShadow(false); setSubtitleLetterSpacing(0.5);
|
||||||
}}
|
}}
|
||||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }}
|
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(34,197,94,0.12)', borderWidth: 1, borderColor: 'rgba(34,197,94,0.35)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={20}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#22C55E', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>High Contrast</Text>
|
<Text style={{ color: '#22C55E', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>High Contrast</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3);
|
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.6); setSubtitleTextShadow(true); setSubtitleOutline(true); setSubtitleAlign('center'); setSubtitleLineHeightMultiplier(1.3);
|
||||||
}}
|
}}
|
||||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }}
|
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 20, backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1, borderColor: 'rgba(59,130,246,0.35)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={20}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#3B82F6', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Large</Text>
|
<Text style={{ color: '#3B82F6', fontWeight: '700', fontSize: isCompact ? 11 : 12 }}>Large</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -283,15 +323,27 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Font Size</Text>
|
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Font Size</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||||
<TouchableOpacity onPress={decreaseSubtitleSize} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', justifyContent: 'center', alignItems: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={decreaseSubtitleSize}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', justifyContent: 'center', alignItems: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="remove" size={18} color="#FFFFFF" />
|
<MaterialIcons name="remove" size={18} color="#FFFFFF" />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<View style={{ minWidth: 42, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
<View style={{ minWidth: 42, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
||||||
<Text style={{ color: '#fff', textAlign: 'center', fontWeight: '700' }}>{subtitleSize}</Text>
|
<Text style={{ color: '#fff', textAlign: 'center', fontWeight: '700' }}>{subtitleSize}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={increaseSubtitleSize} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', justifyContent: 'center', alignItems: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={increaseSubtitleSize}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', justifyContent: 'center', alignItems: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="add" size={18} color="#FFFFFF" />
|
<MaterialIcons name="add" size={18} color="#FFFFFF" />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
|
@ -299,12 +351,15 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
<MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" />
|
<MaterialIcons name="layers" size={16} color="rgba(255,255,255,0.7)" />
|
||||||
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Show Background</Text>
|
<Text style={{ color: '#fff', fontWeight: '600', marginLeft: 8 }}>Show Background</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={{ width: isCompact ? 48 : 54, height: isCompact ? 28 : 30, backgroundColor: subtitleBackground ? 'white' : 'rgba(255,255,255,0.25)', borderRadius: 15, justifyContent: 'center', alignItems: subtitleBackground ? 'flex-end' : 'flex-start', paddingHorizontal: 3 }}
|
style={{ width: isCompact ? 48 : 54, height: isCompact ? 28 : 30, backgroundColor: subtitleBackground ? 'white' : 'rgba(255,255,255,0.25)', borderRadius: 15, justifyContent: 'center', alignItems: subtitleBackground ? 'flex-end' : 'flex-start', paddingHorizontal: 3 }}
|
||||||
onPress={toggleSubtitleBackground}
|
onPress={toggleSubtitleBackground}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={15}
|
||||||
>
|
>
|
||||||
<View style={{ width: 24, height: 24, backgroundColor: subtitleBackground ? 'black' : 'white', borderRadius: 12 }} />
|
<View style={{ width: 24, height: 24, backgroundColor: subtitleBackground ? 'black' : 'white', borderRadius: 12 }} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -321,7 +376,14 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
|
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, justifyContent: 'flex-end' }}>
|
||||||
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
|
{['#FFFFFF', '#FFD700', '#00E5FF', '#FF5C5C', '#00FF88', '#9b59b6', '#f97316'].map(c => (
|
||||||
<TouchableOpacity key={c} onPress={() => setSubtitleTextColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
|
<FocusableTouchableOpacity
|
||||||
|
key={c}
|
||||||
|
onPress={() => setSubtitleTextColor(c)}
|
||||||
|
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleTextColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={11}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -329,95 +391,175 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
<Text style={{ color: 'white', fontWeight: '600' }}>Align</Text>
|
<Text style={{ color: 'white', fontWeight: '600' }}>Align</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||||
{([ { key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' } ] as const).map(a => (
|
{([ { key: 'left', icon: 'format-align-left' }, { key: 'center', icon: 'format-align-center' }, { key: 'right', icon: 'format-align-right' } ] as const).map(a => (
|
||||||
<TouchableOpacity key={a.key} onPress={() => setSubtitleAlign(a.key)} style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}>
|
<FocusableTouchableOpacity
|
||||||
|
key={a.key}
|
||||||
|
onPress={() => setSubtitleAlign(a.key)}
|
||||||
|
style={{ paddingHorizontal: isCompact ? 8 : 10, paddingVertical: isCompact ? 4 : 6, borderRadius: 8, backgroundColor: subtitleAlign === a.key ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={8}
|
||||||
|
>
|
||||||
<MaterialIcons name={a.icon as any} size={18} color="#FFFFFF" />
|
<MaterialIcons name={a.icon as any} size={18} color="#FFFFFF" />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Text style={{ color: 'white', fontWeight: '600' }}>Bottom Offset</Text>
|
<Text style={{ color: 'white', fontWeight: '600' }}>Bottom Offset</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||||
<TouchableOpacity onPress={() => setSubtitleBottomOffset(Math.max(0, subtitleBottomOffset - 5))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleBottomOffset(Math.max(0, subtitleBottomOffset - 5))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="keyboard-arrow-down" color="#fff" size={20} />
|
<MaterialIcons name="keyboard-arrow-down" color="#fff" size={20} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<View style={{ minWidth: 46, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
<View style={{ minWidth: 46, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
||||||
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBottomOffset}</Text>
|
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBottomOffset}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => setSubtitleBottomOffset(subtitleBottomOffset + 5)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleBottomOffset(subtitleBottomOffset + 5)}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="keyboard-arrow-up" color="#fff" size={20} />
|
<MaterialIcons name="keyboard-arrow-up" color="#fff" size={20} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text>
|
<Text style={{ color: 'white', fontWeight: '600' }}>Background Opacity</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||||
<TouchableOpacity onPress={() => setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleBgOpacity(Math.max(0, +(subtitleBgOpacity - 0.1).toFixed(1)))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
||||||
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBgOpacity.toFixed(1)}</Text>
|
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleBgOpacity.toFixed(1)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleBgOpacity(Math.min(1, +(subtitleBgOpacity + 0.1).toFixed(1)))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="add" color="#fff" size={18} />
|
<MaterialIcons name="add" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Text style={{ color: 'white', fontWeight: '600' }}>Text Shadow</Text>
|
<Text style={{ color: 'white', fontWeight: '600' }}>Text Shadow</Text>
|
||||||
<TouchableOpacity onPress={() => setSubtitleTextShadow(!subtitleTextShadow)} style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleTextShadow ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleTextShadow(!subtitleTextShadow)}
|
||||||
|
style={{ paddingHorizontal: 10, paddingVertical: 8, borderRadius: 10, backgroundColor: subtitleTextShadow ? 'rgba(255,255,255,0.18)' : 'rgba(255,255,255,0.08)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)', alignItems: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={10}
|
||||||
|
>
|
||||||
<Text style={{ color: '#fff', fontWeight: '700' }}>{subtitleTextShadow ? 'On' : 'Off'}</Text>
|
<Text style={{ color: '#fff', fontWeight: '700' }}>{subtitleTextShadow ? 'On' : 'Off'}</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Text style={{ color: 'white' }}>Outline Color</Text>
|
<Text style={{ color: 'white' }}>Outline Color</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8 }}>
|
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||||
{['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => (
|
{['#000000', '#FFFFFF', '#00E5FF', '#FF5C5C'].map(c => (
|
||||||
<TouchableOpacity key={c} onPress={() => setSubtitleOutlineColor(c)} style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }} />
|
<FocusableTouchableOpacity
|
||||||
|
key={c}
|
||||||
|
onPress={() => setSubtitleOutlineColor(c)}
|
||||||
|
style={{ width: 22, height: 22, borderRadius: 11, backgroundColor: c, borderWidth: 2, borderColor: subtitleOutlineColor === c ? '#fff' : 'rgba(255,255,255,0.3)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={11}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Text style={{ color: 'white' }}>Outline Width</Text>
|
<Text style={{ color: 'white' }}>Outline Width</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||||
<TouchableOpacity onPress={() => setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleOutlineWidth(Math.max(0, subtitleOutlineWidth - 1))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<View style={{ minWidth: 42, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
<View style={{ minWidth: 42, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
||||||
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOutlineWidth}</Text>
|
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOutlineWidth}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => setSubtitleOutlineWidth(subtitleOutlineWidth + 1)} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleOutlineWidth(subtitleOutlineWidth + 1)}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="add" color="#fff" size={18} />
|
<MaterialIcons name="add" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flexDirection: isCompact ? 'column' : 'row', justifyContent: 'space-between', gap: 12 }}>
|
<View style={{ flexDirection: isCompact ? 'column' : 'row', justifyContent: 'space-between', gap: 12 }}>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={{ color: 'white', fontWeight: '600' }}>Letter Spacing</Text>
|
<Text style={{ color: 'white', fontWeight: '600' }}>Letter Spacing</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<TouchableOpacity onPress={() => setSubtitleLetterSpacing(Math.max(0, +(subtitleLetterSpacing - 0.5).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleLetterSpacing(Math.max(0, +(subtitleLetterSpacing - 0.5).toFixed(1)))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
||||||
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleLetterSpacing.toFixed(1)}</Text>
|
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleLetterSpacing.toFixed(1)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => setSubtitleLetterSpacing(+(subtitleLetterSpacing + 0.5).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleLetterSpacing(+(subtitleLetterSpacing + 0.5).toFixed(1))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="add" color="#fff" size={18} />
|
<MaterialIcons name="add" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={{ color: 'white', fontWeight: '600' }}>Line Height</Text>
|
<Text style={{ color: 'white', fontWeight: '600' }}>Line Height</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<TouchableOpacity onPress={() => setSubtitleLineHeightMultiplier(Math.max(1, +(subtitleLineHeightMultiplier - 0.1).toFixed(1)))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleLineHeightMultiplier(Math.max(1, +(subtitleLineHeightMultiplier - 0.1).toFixed(1)))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
<View style={{ minWidth: 48, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
||||||
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleLineHeightMultiplier.toFixed(1)}</Text>
|
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleLineHeightMultiplier.toFixed(1)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => setSubtitleLineHeightMultiplier(+(subtitleLineHeightMultiplier + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleLineHeightMultiplier(+(subtitleLineHeightMultiplier + 0.1).toFixed(1))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="add" color="#fff" size={18} />
|
<MaterialIcons name="add" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -425,21 +567,33 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text>
|
<Text style={{ color: 'white', fontWeight: '600' }}>Timing Offset (s)</Text>
|
||||||
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 8, alignItems: 'center' }}>
|
||||||
<TouchableOpacity onPress={() => setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleOffsetSec(+(subtitleOffsetSec - 0.1).toFixed(1))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="remove" color="#fff" size={18} />
|
<MaterialIcons name="remove" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<View style={{ minWidth: 60, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
<View style={{ minWidth: 60, paddingHorizontal: 6, paddingVertical: 4, borderRadius: 10, backgroundColor: 'rgba(255,255,255,0.12)' }}>
|
||||||
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOffsetSec.toFixed(1)}</Text>
|
<Text style={{ color: 'white', textAlign: 'center', fontWeight: '700' }}>{subtitleOffsetSec.toFixed(1)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity onPress={() => setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))} style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => setSubtitleOffsetSec(+(subtitleOffsetSec + 0.1).toFixed(1))}
|
||||||
|
style={{ width: controlBtn.size, height: controlBtn.size, borderRadius: controlBtn.radius, backgroundColor: 'rgba(255,255,255,0.18)', alignItems: 'center', justifyContent: 'center' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={controlBtn.radius}
|
||||||
|
>
|
||||||
<MaterialIcons name="add" color="#fff" size={18} />
|
<MaterialIcons name="add" color="#fff" size={18} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 6 }}>Nudge subtitles earlier (-) or later (+) to sync if needed.</Text>
|
<Text style={{ color: 'rgba(255,255,255,0.6)', fontSize: 11, marginTop: 6 }}>Nudge subtitles earlier (-) or later (+) to sync if needed.</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={{ alignItems: 'flex-end', marginTop: 8 }}>
|
<View style={{ alignItems: 'flex-end', marginTop: 8 }}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
|
setSubtitleTextColor('#FFFFFF'); setSubtitleBgOpacity(0.7); setSubtitleTextShadow(true);
|
||||||
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
|
setSubtitleOutline(true); setSubtitleOutlineColor('#000000'); setSubtitleOutlineWidth(4);
|
||||||
|
|
@ -447,9 +601,12 @@ export const SubtitleModals: React.FC<SubtitleModalsProps> = ({
|
||||||
setSubtitleLineHeightMultiplier(1.2); setSubtitleOffsetSec(0);
|
setSubtitleLineHeightMultiplier(1.2); setSubtitleOffsetSec(0);
|
||||||
}}
|
}}
|
||||||
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 8, backgroundColor: 'rgba(255,255,255,0.1)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
|
style={{ paddingHorizontal: chipPadH, paddingVertical: chipPadV, borderRadius: 8, backgroundColor: 'rgba(255,255,255,0.1)', borderWidth: 1, borderColor: 'rgba(255,255,255,0.15)' }}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={8}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 12 : 14 }}>Reset to defaults</Text>
|
<Text style={{ color: '#fff', fontWeight: '600', fontSize: isCompact ? 12 : 14 }}>Reset to defaults</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import {
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
|
|
@ -58,6 +57,7 @@ import Animated, {
|
||||||
Extrapolate,
|
Extrapolate,
|
||||||
runOnJS
|
runOnJS
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
const isTablet = width >= 768;
|
const isTablet = width >= 768;
|
||||||
|
|
@ -302,7 +302,7 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.suggestionChip, { backgroundColor: currentTheme.colors.elevation1 }]}
|
style={[styles.suggestionChip, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -310,7 +310,7 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
|
||||||
<Text style={[styles.suggestionText, { color: currentTheme.colors.primary }]}>
|
<Text style={[styles.suggestionText, { color: currentTheme.colors.primary }]}>
|
||||||
{text}
|
{text}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress);
|
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress);
|
||||||
|
|
||||||
|
|
@ -684,7 +684,7 @@ const AIChatScreen: React.FC = () => {
|
||||||
headerAnimatedStyle
|
headerAnimatedStyle
|
||||||
]}>
|
]}>
|
||||||
<View style={styles.headerContent}>
|
<View style={styles.headerContent}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => {
|
modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => {
|
||||||
|
|
@ -697,7 +697,7 @@ const AIChatScreen: React.FC = () => {
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerInfo}>
|
<View style={styles.headerInfo}>
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
|
|
@ -821,7 +821,7 @@ const AIChatScreen: React.FC = () => {
|
||||||
blurOnSubmit={false}
|
blurOnSubmit={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.sendButton,
|
styles.sendButton,
|
||||||
{
|
{
|
||||||
|
|
@ -837,7 +837,7 @@ const AIChatScreen: React.FC = () => {
|
||||||
size={20}
|
size={20}
|
||||||
color={inputText.trim() ? 'white' : currentTheme.colors.mediumEmphasis}
|
color={inputText.trim() ? 'white' : currentTheme.colors.mediumEmphasis}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native';
|
import { View, Text, StyleSheet, StatusBar, Platform, Animated, Easing, TextInput, ActivityIndicator } from 'react-native';
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
@ -9,6 +9,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const AccountManageScreen: React.FC = () => {
|
const AccountManageScreen: React.FC = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
@ -97,9 +98,9 @@ const AccountManageScreen: React.FC = () => {
|
||||||
colors={[currentTheme.colors.darkBackground, '#111318']}
|
colors={[currentTheme.colors.darkBackground, '#111318']}
|
||||||
style={StyleSheet.absoluteFill}
|
style={StyleSheet.absoluteFill}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.headerBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
<FocusableTouchableOpacity onPress={() => navigation.goBack()} style={styles.headerBack} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||||
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
|
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Account</Text>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.white }]}>Account</Text>
|
||||||
<View style={{ width: 22, height: 22 }} />
|
<View style={{ width: 22, height: 22 }} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
@ -185,7 +186,7 @@ const AccountManageScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Save and Sign out */}
|
{/* Save and Sign out */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
style={[styles.saveButton, { backgroundColor: currentTheme.colors.elevation2, borderColor: currentTheme.colors.elevation2 }]}
|
style={[styles.saveButton, { backgroundColor: currentTheme.colors.elevation2, borderColor: currentTheme.colors.elevation2 }]}
|
||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
|
|
@ -199,9 +200,9 @@ const AccountManageScreen: React.FC = () => {
|
||||||
<Text style={styles.saveText}>Save changes</Text>
|
<Text style={styles.saveText}>Save changes</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
style={[
|
style={[
|
||||||
styles.signOutButton,
|
styles.signOutButton,
|
||||||
|
|
@ -211,7 +212,7 @@ const AccountManageScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<MaterialIcons name="logout" size={18} color="#fff" style={{ marginRight: 8 }} />
|
<MaterialIcons name="logout" size={18} color="#fff" style={{ marginRight: 8 }} />
|
||||||
<Text style={styles.signOutText}>Sign out</Text>
|
<Text style={styles.signOutText}>Sign out</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<CustomAlert
|
<CustomAlert
|
||||||
visible={alertVisible}
|
visible={alertVisible}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
FlatList,
|
FlatList,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
|
@ -48,6 +47,7 @@ if (Platform.OS === 'ios') {
|
||||||
// Removed community blur and expo-constants for Android overlay
|
// Removed community blur and expo-constants for Android overlay
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Extend Manifest type to include logo only (remove disabled status)
|
// Extend Manifest type to include logo only (remove disabled status)
|
||||||
interface ExtendedManifest extends Manifest {
|
interface ExtendedManifest extends Manifest {
|
||||||
|
|
@ -974,7 +974,7 @@ const AddonsScreen = () => {
|
||||||
<View style={styles.addonItem}>
|
<View style={styles.addonItem}>
|
||||||
{reorderMode && (
|
{reorderMode && (
|
||||||
<View style={styles.reorderButtons}>
|
<View style={styles.reorderButtons}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.reorderButton, isFirstItem && styles.disabledButton]}
|
style={[styles.reorderButton, isFirstItem && styles.disabledButton]}
|
||||||
onPress={() => moveAddonUp(item)}
|
onPress={() => moveAddonUp(item)}
|
||||||
disabled={isFirstItem}
|
disabled={isFirstItem}
|
||||||
|
|
@ -984,8 +984,8 @@ const AddonsScreen = () => {
|
||||||
size={20}
|
size={20}
|
||||||
color={isFirstItem ? colors.mediumGray : colors.white}
|
color={isFirstItem ? colors.mediumGray : colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.reorderButton, isLastItem && styles.disabledButton]}
|
style={[styles.reorderButton, isLastItem && styles.disabledButton]}
|
||||||
onPress={() => moveAddonDown(item)}
|
onPress={() => moveAddonDown(item)}
|
||||||
disabled={isLastItem}
|
disabled={isLastItem}
|
||||||
|
|
@ -995,7 +995,7 @@ const AddonsScreen = () => {
|
||||||
size={20}
|
size={20}
|
||||||
color={isLastItem ? colors.mediumGray : colors.white}
|
color={isLastItem ? colors.mediumGray : colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1030,20 +1030,20 @@ const AddonsScreen = () => {
|
||||||
{!reorderMode ? (
|
{!reorderMode ? (
|
||||||
<>
|
<>
|
||||||
{isConfigurable && (
|
{isConfigurable && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.configButton}
|
style={styles.configButton}
|
||||||
onPress={() => handleConfigureAddon(item, item.transport)}
|
onPress={() => handleConfigureAddon(item, item.transport)}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{!stremioService.isPreInstalledAddon(item.id) && (
|
{!stremioService.isPreInstalledAddon(item.id) && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.deleteButton}
|
style={styles.deleteButton}
|
||||||
onPress={() => handleRemoveAddon(item)}
|
onPress={() => handleRemoveAddon(item)}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="delete" size={20} color={colors.error} />
|
<MaterialIcons name="delete" size={20} color={colors.error} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -1098,14 +1098,14 @@ const AddonsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.addonActionButtons}>
|
<View style={styles.addonActionButtons}>
|
||||||
{isConfigurable && (
|
{isConfigurable && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.configButton}
|
style={styles.configButton}
|
||||||
onPress={() => handleConfigureAddon(manifest, transportUrl)}
|
onPress={() => handleConfigureAddon(manifest, transportUrl)}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||||
onPress={() => handleAddAddon(transportUrl)}
|
onPress={() => handleAddAddon(transportUrl)}
|
||||||
disabled={installing}
|
disabled={installing}
|
||||||
|
|
@ -1115,7 +1115,7 @@ const AddonsScreen = () => {
|
||||||
) : (
|
) : (
|
||||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -1134,17 +1134,17 @@ const AddonsScreen = () => {
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||||
<Text style={styles.backText}>Settings</Text>
|
<Text style={styles.backText}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
{/* Reorder Mode Toggle Button */}
|
{/* Reorder Mode Toggle Button */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.headerButton, reorderMode && styles.activeHeaderButton]}
|
style={[styles.headerButton, reorderMode && styles.activeHeaderButton]}
|
||||||
onPress={toggleReorderMode}
|
onPress={toggleReorderMode}
|
||||||
>
|
>
|
||||||
|
|
@ -1153,10 +1153,10 @@ const AddonsScreen = () => {
|
||||||
size={24}
|
size={24}
|
||||||
color={reorderMode ? colors.primary : colors.white}
|
color={reorderMode ? colors.primary : colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{/* Refresh Button */}
|
{/* Refresh Button */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.headerButton}
|
style={styles.headerButton}
|
||||||
onPress={refreshAddons}
|
onPress={refreshAddons}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|
@ -1166,7 +1166,7 @@ const AddonsScreen = () => {
|
||||||
size={24}
|
size={24}
|
||||||
color={loading ? colors.mediumGray : colors.white}
|
color={loading ? colors.mediumGray : colors.white}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -1221,7 +1221,7 @@ const AddonsScreen = () => {
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.addButton, { opacity: installing || !addonUrl ? 0.6 : 1 }]}
|
style={[styles.addButton, { opacity: installing || !addonUrl ? 0.6 : 1 }]}
|
||||||
onPress={() => handleAddAddon()}
|
onPress={() => handleAddAddon()}
|
||||||
disabled={installing || !addonUrl}
|
disabled={installing || !addonUrl}
|
||||||
|
|
@ -1229,7 +1229,7 @@ const AddonsScreen = () => {
|
||||||
<Text style={styles.addButtonText}>
|
<Text style={styles.addButtonText}>
|
||||||
{installing ? 'Loading...' : 'Add Addon'}
|
{installing ? 'Loading...' : 'Add Addon'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1289,14 +1289,14 @@ const AddonsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.addonActions}>
|
<View style={styles.addonActions}>
|
||||||
{promoAddon.behaviorHints?.configurable && (
|
{promoAddon.behaviorHints?.configurable && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.configButton}
|
style={styles.configButton}
|
||||||
onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
|
onPress={() => handleConfigureAddon(promoAddon, PROMO_ADDON_URL)}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.installButton}
|
style={styles.installButton}
|
||||||
onPress={() => handleAddAddon(PROMO_ADDON_URL)}
|
onPress={() => handleAddAddon(PROMO_ADDON_URL)}
|
||||||
disabled={installing}
|
disabled={installing}
|
||||||
|
|
@ -1306,7 +1306,7 @@ const AddonsScreen = () => {
|
||||||
) : (
|
) : (
|
||||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.addonDescription}>
|
<Text style={styles.addonDescription}>
|
||||||
|
|
@ -1371,14 +1371,14 @@ const AddonsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.addonActions}>
|
<View style={styles.addonActions}>
|
||||||
{item.manifest.behaviorHints?.configurable && (
|
{item.manifest.behaviorHints?.configurable && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.configButton}
|
style={styles.configButton}
|
||||||
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
|
onPress={() => handleConfigureAddon(item.manifest, item.transportUrl)}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
<MaterialIcons name="settings" size={20} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
style={[styles.installButton, installing && { opacity: 0.6 }]}
|
||||||
onPress={() => handleAddAddon(item.transportUrl)}
|
onPress={() => handleAddAddon(item.transportUrl)}
|
||||||
disabled={installing}
|
disabled={installing}
|
||||||
|
|
@ -1388,7 +1388,7 @@ const AddonsScreen = () => {
|
||||||
) : (
|
) : (
|
||||||
<MaterialIcons name="add" size={20} color={colors.white} />
|
<MaterialIcons name="add" size={20} color={colors.white} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -1435,14 +1435,14 @@ const AddonsScreen = () => {
|
||||||
<>
|
<>
|
||||||
<View style={styles.modalHeader}>
|
<View style={styles.modalHeader}>
|
||||||
<Text style={styles.modalTitle}>Install Addon</Text>
|
<Text style={styles.modalTitle}>Install Addon</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowConfirmModal(false);
|
setShowConfirmModal(false);
|
||||||
setAddonDetails(null);
|
setAddonDetails(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="close" size={24} color={colors.white} />
|
<MaterialIcons name="close" size={24} color={colors.white} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
|
@ -1504,7 +1504,7 @@ const AddonsScreen = () => {
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<View style={styles.modalActions}>
|
<View style={styles.modalActions}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.modalButton, styles.cancelButton]}
|
style={[styles.modalButton, styles.cancelButton]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowConfirmModal(false);
|
setShowConfirmModal(false);
|
||||||
|
|
@ -1512,8 +1512,8 @@ const AddonsScreen = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.modalButtonText}>Cancel</Text>
|
<Text style={styles.modalButtonText}>Cancel</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.modalButton, styles.installButton]}
|
style={[styles.modalButton, styles.installButton]}
|
||||||
onPress={confirmInstallAddon}
|
onPress={confirmInstallAddon}
|
||||||
disabled={installing}
|
disabled={installing}
|
||||||
|
|
@ -1523,7 +1523,7 @@ const AddonsScreen = () => {
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.modalButtonText}>Install</Text>
|
<Text style={styles.modalButtonText}>Install</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { View, TextInput, Text, TouchableOpacity, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Dimensions, Animated, Easing, Keyboard } from 'react-native';
|
import { View, TextInput, Text, StyleSheet, ActivityIndicator, SafeAreaView, KeyboardAvoidingView, Platform, Dimensions, Animated, Easing, Keyboard } from 'react-native';
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
|
@ -9,6 +9,7 @@ import { useNavigation, useRoute } from '@react-navigation/native';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -277,9 +278,9 @@ const AuthScreen: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{navigation.canGoBack() && (
|
{navigation.canGoBack() && (
|
||||||
<TouchableOpacity onPress={() => navigation.goBack()} style={[styles.backButton, Platform.OS === 'android' ? { top: Math.max(insets.top + 6, 18) } : null]} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
<FocusableTouchableOpacity onPress={() => navigation.goBack()} style={[styles.backButton, Platform.OS === 'android' ? { top: Math.max(insets.top + 6, 18) } : null]} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}>
|
||||||
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
|
<MaterialIcons name="arrow-back" size={22} color={currentTheme.colors.white} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<Animated.Text style={[styles.heading, { color: currentTheme.colors.white, opacity: titleOpacity, transform: [{ translateY: titleTranslateY }] }]}>
|
<Animated.Text style={[styles.heading, { color: currentTheme.colors.white, opacity: titleOpacity, transform: [{ translateY: titleTranslateY }] }]}>
|
||||||
{mode === 'signin' ? 'Welcome back' : 'Create your account'}
|
{mode === 'signin' ? 'Welcome back' : 'Create your account'}
|
||||||
|
|
@ -299,7 +300,7 @@ const AuthScreen: React.FC = () => {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.warningCard, { backgroundColor: 'rgba(255, 193, 7, 0.1)', borderColor: 'rgba(255, 193, 7, 0.3)' }]}
|
style={[styles.warningCard, { backgroundColor: 'rgba(255, 193, 7, 0.1)', borderColor: 'rgba(255, 193, 7, 0.3)' }]}
|
||||||
onPress={toggleWarningDetails}
|
onPress={toggleWarningDetails}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
|
|
@ -316,7 +317,7 @@ const AuthScreen: React.FC = () => {
|
||||||
Read more {showWarningDetails ? '▼' : '▶'}
|
Read more {showWarningDetails ? '▼' : '▶'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{/* Expanded Details */}
|
{/* Expanded Details */}
|
||||||
{showWarningDetails && (
|
{showWarningDetails && (
|
||||||
|
|
@ -392,7 +393,7 @@ const AuthScreen: React.FC = () => {
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.switchButton,
|
styles.switchButton,
|
||||||
]}
|
]}
|
||||||
|
|
@ -402,8 +403,8 @@ const AuthScreen: React.FC = () => {
|
||||||
<Text style={[styles.switchText, { color: mode === 'signin' ? '#fff' : currentTheme.colors.textMuted }]}>
|
<Text style={[styles.switchText, { color: mode === 'signin' ? '#fff' : currentTheme.colors.textMuted }]}>
|
||||||
Sign In
|
Sign In
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.switchButton,
|
styles.switchButton,
|
||||||
signupDisabled && styles.disabledButton,
|
signupDisabled && styles.disabledButton,
|
||||||
|
|
@ -420,7 +421,7 @@ const AuthScreen: React.FC = () => {
|
||||||
]}>
|
]}>
|
||||||
Sign Up {signupDisabled && '(Disabled)'}
|
Sign Up {signupDisabled && '(Disabled)'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Email Input */}
|
{/* Email Input */}
|
||||||
|
|
@ -477,13 +478,13 @@ const AuthScreen: React.FC = () => {
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
onSubmitEditing={handleSubmit}
|
onSubmitEditing={handleSubmit}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity onPress={() => setShowPassword(p => !p)} style={styles.eyeButton}>
|
<FocusableTouchableOpacity onPress={() => setShowPassword(p => !p)} style={styles.eyeButton}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={showPassword ? 'visibility-off' : 'visibility'}
|
name={showPassword ? 'visibility-off' : 'visibility'}
|
||||||
size={16}
|
size={16}
|
||||||
color={currentTheme.colors.textMuted}
|
color={currentTheme.colors.textMuted}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{Platform.OS !== 'android' && isPasswordValid && (
|
{Platform.OS !== 'android' && isPasswordValid && (
|
||||||
<MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} />
|
<MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -515,13 +516,13 @@ const AuthScreen: React.FC = () => {
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
onSubmitEditing={handleSubmit}
|
onSubmitEditing={handleSubmit}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity onPress={() => setShowConfirm(p => !p)} style={styles.eyeButton}>
|
<FocusableTouchableOpacity onPress={() => setShowConfirm(p => !p)} style={styles.eyeButton}>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name={showConfirm ? 'visibility-off' : 'visibility'}
|
name={showConfirm ? 'visibility-off' : 'visibility'}
|
||||||
size={16}
|
size={16}
|
||||||
color={currentTheme.colors.textMuted}
|
color={currentTheme.colors.textMuted}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{Platform.OS !== 'android' && passwordsMatch && isConfirmValid && (
|
{Platform.OS !== 'android' && passwordsMatch && isConfirmValid && (
|
||||||
<MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} />
|
<MaterialIcons name="check-circle" size={16} color="#2EA043" style={{ marginRight: 12 }} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -539,7 +540,7 @@ const AuthScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<Animated.View style={{ transform: [{ scale: ctaScale }] }}>
|
<Animated.View style={{ transform: [{ scale: ctaScale }] }}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.ctaButton,
|
styles.ctaButton,
|
||||||
{
|
{
|
||||||
|
|
@ -579,12 +580,12 @@ const AuthScreen: React.FC = () => {
|
||||||
{mode === 'signin' ? 'Sign In' : 'Create Account'}
|
{mode === 'signin' ? 'Sign In' : 'Create Account'}
|
||||||
</Animated.Text>
|
</Animated.Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* Switch Mode */}
|
{/* Switch Mode */}
|
||||||
{!signupDisabled && (
|
{!signupDisabled && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
|
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
style={{ marginTop: 16 }}
|
style={{ marginTop: 16 }}
|
||||||
|
|
@ -595,7 +596,7 @@ const AuthScreen: React.FC = () => {
|
||||||
{mode === 'signin' ? 'Sign up' : 'Sign in'}
|
{mode === 'signin' ? 'Sign up' : 'Sign in'}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Signup disabled message */}
|
{/* Signup disabled message */}
|
||||||
|
|
@ -608,7 +609,7 @@ const AuthScreen: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Skip sign in - more prominent when coming from onboarding */}
|
{/* Skip sign in - more prominent when coming from onboarding */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={handleSkipAuth}
|
onPress={handleSkipAuth}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -629,7 +630,7 @@ const AuthScreen: React.FC = () => {
|
||||||
}}>
|
}}>
|
||||||
Continue without an account
|
Continue without an account
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import {
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
FlatList,
|
FlatList,
|
||||||
TouchableOpacity,
|
|
||||||
Dimensions,
|
Dimensions,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
|
@ -16,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { TMDBService } from '../services/tmdbService';
|
import { TMDBService } from '../services/tmdbService';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
const BACKDROP_WIDTH = width * 0.9;
|
const BACKDROP_WIDTH = width * 0.9;
|
||||||
|
|
@ -116,12 +117,16 @@ const BackdropGalleryScreen: React.FC = () => {
|
||||||
|
|
||||||
const renderHeader = () => (
|
const renderHeader = () => (
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={999}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
|
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.highEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>
|
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>
|
||||||
{title}
|
{title}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Platform,
|
Platform,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
|
|
@ -21,6 +20,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
import { useBackupOptions } from '../hooks/useBackupOptions';
|
import { useBackupOptions } from '../hooks/useBackupOptions';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Check if running on TV platform
|
// Check if running on TV platform
|
||||||
const isTV = Platform.isTV;
|
const isTV = Platform.isTV;
|
||||||
|
|
@ -303,13 +303,13 @@ const BackupScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.white} />
|
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.white} />
|
||||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
{/* Empty for now, but keeping structure consistent */}
|
{/* Empty for now, but keeping structure consistent */}
|
||||||
|
|
@ -345,7 +345,7 @@ const BackupScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Core Data Group */}
|
{/* Core Data Group */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.sectionHeader}
|
style={styles.sectionHeader}
|
||||||
onPress={() => toggleSection('coreData')}
|
onPress={() => toggleSection('coreData')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -365,7 +365,7 @@ const BackupScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
maxHeight: coreDataAnim.interpolate({
|
maxHeight: coreDataAnim.interpolate({
|
||||||
|
|
@ -393,7 +393,7 @@ const BackupScreen: React.FC = () => {
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* Addons & Integrations Group */}
|
{/* Addons & Integrations Group */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.sectionHeader}
|
style={styles.sectionHeader}
|
||||||
onPress={() => toggleSection('addonsIntegrations')}
|
onPress={() => toggleSection('addonsIntegrations')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -413,7 +413,7 @@ const BackupScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
maxHeight: addonsAnim.interpolate({
|
maxHeight: addonsAnim.interpolate({
|
||||||
|
|
@ -448,7 +448,7 @@ const BackupScreen: React.FC = () => {
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{/* Settings & Preferences Group */}
|
{/* Settings & Preferences Group */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.sectionHeader}
|
style={styles.sectionHeader}
|
||||||
onPress={() => toggleSection('settingsPreferences')}
|
onPress={() => toggleSection('settingsPreferences')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -468,7 +468,7 @@ const BackupScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
<MaterialIcons name="expand-more" size={24} color={currentTheme.colors.highEmphasis} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
maxHeight: settingsAnim.interpolate({
|
maxHeight: settingsAnim.interpolate({
|
||||||
|
|
@ -516,7 +516,7 @@ const BackupScreen: React.FC = () => {
|
||||||
Backup & Restore
|
Backup & Restore
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.actionButton,
|
styles.actionButton,
|
||||||
{
|
{
|
||||||
|
|
@ -535,9 +535,9 @@ const BackupScreen: React.FC = () => {
|
||||||
<Text style={styles.actionButtonText}>Create Backup</Text>
|
<Text style={styles.actionButtonText}>Create Backup</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.actionButton,
|
styles.actionButton,
|
||||||
{
|
{
|
||||||
|
|
@ -550,7 +550,7 @@ const BackupScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<MaterialIcons name="restore" size={20} color="white" />
|
<MaterialIcons name="restore" size={20} color="white" />
|
||||||
<Text style={styles.actionButtonText}>Restore from Backup</Text>
|
<Text style={styles.actionButtonText}>Restore from Backup</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Info Section */}
|
{/* Info Section */}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
|
@ -42,6 +41,7 @@ import { useCustomCatalogNames } from '../hooks/useCustomCatalogNames';
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
|
import { catalogService, DataSource, StreamingContent } from '../services/catalogService';
|
||||||
import { tmdbService } from '../services/tmdbService';
|
import { tmdbService } from '../services/tmdbService';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
type CatalogScreenProps = {
|
type CatalogScreenProps = {
|
||||||
route: RouteProp<RootStackParamList, 'Catalog'>;
|
route: RouteProp<RootStackParamList, 'Catalog'>;
|
||||||
|
|
@ -762,7 +762,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
const aspectRatio = shape === 'landscape' ? 16 / 9 : (shape === 'square' ? 1 : 2 / 3);
|
const aspectRatio = shape === 'landscape' ? 16 / 9 : (shape === 'square' ? 1 : 2 / 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.item,
|
styles.item,
|
||||||
{
|
{
|
||||||
|
|
@ -772,6 +772,9 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
]}
|
]}
|
||||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type, addonId })}
|
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type, addonId })}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="poster"
|
||||||
|
focusBorderRadius={12}
|
||||||
>
|
>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: optimizePosterUrl(item.poster) }}
|
source={{ uri: optimizePosterUrl(item.poster) }}
|
||||||
|
|
@ -837,7 +840,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
}, [navigation, styles, effectiveNumColumns, effectiveItemWidth, screenData, type, nowPlayingMovies, colors.white, colors.mediumGray, optimizePosterUrl, addonId, isDarkMode, showTitles]);
|
}, [navigation, styles, effectiveNumColumns, effectiveItemWidth, screenData, type, nowPlayingMovies, colors.white, colors.mediumGray, optimizePosterUrl, addonId, isDarkMode, showTitles]);
|
||||||
|
|
||||||
|
|
@ -847,12 +850,16 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<Text style={styles.emptyText}>
|
<Text style={styles.emptyText}>
|
||||||
No content found
|
No content found
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={handleRefresh}
|
onPress={handleRefresh}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Try Again</Text>
|
<Text style={styles.buttonText}>Try Again</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -862,12 +869,16 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<Text style={styles.errorText}>
|
<Text style={styles.errorText}>
|
||||||
{error}
|
{error}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
onPress={() => loadItems(true)}
|
onPress={() => loadItems(true)}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Retry</Text>
|
<Text style={styles.buttonText}>Retry</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -885,13 +896,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||||
<Text style={styles.backText}>Back</Text>
|
<Text style={styles.backText}>Back</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.headerTitle}>{displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
<Text style={styles.headerTitle}>{displayName || originalName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||||
{renderLoadingState()}
|
{renderLoadingState()}
|
||||||
|
|
@ -904,13 +919,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||||
<Text style={styles.backText}>Back</Text>
|
<Text style={styles.backText}>Back</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||||
{renderErrorState()}
|
{renderErrorState()}
|
||||||
|
|
@ -922,13 +941,17 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||||
<Text style={styles.backText}>Back</Text>
|
<Text style={styles.backText}>Back</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
<Text style={styles.headerTitle}>{displayName || `${type.charAt(0).toUpperCase() + type.slice(1)}s`}</Text>
|
||||||
|
|
||||||
|
|
@ -943,18 +966,21 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
{catalogExtras.map(extra => (
|
{catalogExtras.map(extra => (
|
||||||
<React.Fragment key={extra.name}>
|
<React.Fragment key={extra.name}>
|
||||||
{/* All option - clears filter */}
|
{/* All option - clears filter */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.filterChip,
|
styles.filterChip,
|
||||||
(extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipActive
|
(extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipActive
|
||||||
]}
|
]}
|
||||||
onPress={() => handleFilterChange(extra.name, undefined)}
|
onPress={() => handleFilterChange(extra.name, undefined)}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.filterChipText,
|
styles.filterChipText,
|
||||||
(extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipTextActive
|
(extra.name === 'genre' ? !activeGenreFilter : !selectedFilters[extra.name]) && styles.filterChipTextActive
|
||||||
]}>All</Text>
|
]}>All</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{/* Filter options from catalog extra */}
|
{/* Filter options from catalog extra */}
|
||||||
{extra.options?.map(option => {
|
{extra.options?.map(option => {
|
||||||
|
|
@ -962,15 +988,19 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||||
? activeGenreFilter === option
|
? activeGenreFilter === option
|
||||||
: selectedFilters[extra.name] === option;
|
: selectedFilters[extra.name] === option;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={option}
|
key={option}
|
||||||
style={[styles.filterChip, isActive && styles.filterChipActive]}
|
style={[styles.filterChip, isActive && styles.filterChipActive]}
|
||||||
onPress={() => handleFilterChange(extra.name, option)}
|
onPress={() => handleFilterChange(extra.name, option)}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && isActive}
|
||||||
>
|
>
|
||||||
<Text style={[styles.filterChipText, isActive && styles.filterChipTextActive]}>
|
<Text style={[styles.filterChipText, isActive && styles.filterChipTextActive]}>
|
||||||
{option}
|
{option}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Platform,
|
Platform,
|
||||||
|
|
@ -17,6 +16,7 @@ import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// TTL options in milliseconds - organized in rows
|
// TTL options in milliseconds - organized in rows
|
||||||
const TTL_OPTIONS = [
|
const TTL_OPTIONS = [
|
||||||
|
|
@ -132,7 +132,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => {
|
const TTLPickerItem = ({ option }: { option: { label: string; value: number } }) => {
|
||||||
const isSelected = settings.streamCacheTTL === option.value;
|
const isSelected = settings.streamCacheTTL === option.value;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.ttlOption,
|
styles.ttlOption,
|
||||||
{
|
{
|
||||||
|
|
@ -142,6 +142,10 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
onPress={() => handleUpdateSetting('streamCacheTTL', option.value)}
|
onPress={() => handleUpdateSetting('streamCacheTTL', option.value)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={8}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && isSelected}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.ttlOptionText,
|
styles.ttlOptionText,
|
||||||
|
|
@ -152,7 +156,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<MaterialIcons name="check" size={20} color={colors.white} />
|
<MaterialIcons name="check" size={20} color={colors.white} />
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -162,13 +166,17 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={handleBack}
|
onPress={handleBack}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
|
||||||
<Text style={styles.backText}>Settings</Text>
|
<Text style={styles.backText}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.headerTitle}>
|
<Text style={styles.headerTitle}>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
|
@ -24,6 +23,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { fetchContributors, GitHubContributor } from '../services/githubReleaseService';
|
import { fetchContributors, GitHubContributor } from '../services/githubReleaseService';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
const isTablet = width >= 768;
|
const isTablet = width >= 768;
|
||||||
|
|
@ -91,7 +91,7 @@ const ContributorCard: React.FC<ContributorCardProps> = ({ contributor, currentT
|
||||||
}, [contributor.html_url]);
|
}, [contributor.html_url]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.contributorCard,
|
styles.contributorCard,
|
||||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||||
|
|
@ -130,7 +130,7 @@ const ContributorCard: React.FC<ContributorCardProps> = ({ contributor, currentT
|
||||||
color={currentTheme.colors.mediumEmphasis}
|
color={currentTheme.colors.mediumEmphasis}
|
||||||
style={styles.externalIcon}
|
style={styles.externalIcon}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -164,7 +164,7 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
|
||||||
const defaultAvatar = `https://cdn.discordapp.com/embed/avatars/0.png`;
|
const defaultAvatar = `https://cdn.discordapp.com/embed/avatars/0.png`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.contributorCard,
|
styles.contributorCard,
|
||||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||||
|
|
@ -230,7 +230,7 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
|
||||||
color={currentTheme.colors.mediumEmphasis}
|
color={currentTheme.colors.mediumEmphasis}
|
||||||
style={styles.externalIcon}
|
style={styles.externalIcon}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -422,13 +422,13 @@ const ContributorsScreen: React.FC = () => {
|
||||||
<StatusBar barStyle={'light-content'} />
|
<StatusBar barStyle={'light-content'} />
|
||||||
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
|
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
|
||||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.headerTitle,
|
styles.headerTitle,
|
||||||
|
|
@ -457,13 +457,13 @@ const ContributorsScreen: React.FC = () => {
|
||||||
|
|
||||||
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
|
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
|
||||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.headerTitle,
|
styles.headerTitle,
|
||||||
|
|
@ -480,7 +480,7 @@ const ContributorsScreen: React.FC = () => {
|
||||||
{ backgroundColor: currentTheme.colors.elevation1 },
|
{ backgroundColor: currentTheme.colors.elevation1 },
|
||||||
isTablet && styles.tabletTabSwitcher
|
isTablet && styles.tabletTabSwitcher
|
||||||
]}>
|
]}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.tab,
|
styles.tab,
|
||||||
activeTab === 'contributors' && { backgroundColor: currentTheme.colors.primary },
|
activeTab === 'contributors' && { backgroundColor: currentTheme.colors.primary },
|
||||||
|
|
@ -496,8 +496,8 @@ const ContributorsScreen: React.FC = () => {
|
||||||
]}>
|
]}>
|
||||||
Contributors
|
Contributors
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.tab,
|
styles.tab,
|
||||||
activeTab === 'special' && { backgroundColor: currentTheme.colors.primary },
|
activeTab === 'special' && { backgroundColor: currentTheme.colors.primary },
|
||||||
|
|
@ -513,7 +513,7 @@ const ContributorsScreen: React.FC = () => {
|
||||||
]}>
|
]}>
|
||||||
Special Mentions
|
Special Mentions
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
|
|
@ -530,14 +530,14 @@ const ContributorsScreen: React.FC = () => {
|
||||||
<Text style={[styles.errorSubtext, { color: currentTheme.colors.mediumEmphasis }]}>
|
<Text style={[styles.errorSubtext, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
GitHub API rate limit exceeded. Please try again later or pull to refresh.
|
GitHub API rate limit exceeded. Please try again later or pull to refresh.
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={() => loadContributors()}
|
onPress={() => loadContributors()}
|
||||||
>
|
>
|
||||||
<Text style={[styles.retryText, { color: currentTheme.colors.white }]}>
|
<Text style={[styles.retryText, { color: currentTheme.colors.white }]}>
|
||||||
Try Again
|
Try Again
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : contributors.length === 0 ? (
|
) : contributors.length === 0 ? (
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import {
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Platform,
|
Platform,
|
||||||
|
|
@ -27,6 +26,7 @@ import { logger } from '../utils/logger';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
const TORBOX_STORAGE_KEY = 'torbox_debrid_config';
|
const TORBOX_STORAGE_KEY = 'torbox_debrid_config';
|
||||||
|
|
@ -1132,15 +1132,18 @@ const DebridIntegrationScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.actionButton, styles.dangerButton, loading && styles.disabledButton]}
|
style={[styles.actionButton, styles.dangerButton, loading && styles.disabledButton]}
|
||||||
onPress={handleDisconnect}
|
onPress={handleDisconnect}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>
|
<Text style={styles.buttonText}>
|
||||||
{loading ? 'Disconnecting...' : 'Disconnect & Remove'}
|
{loading ? 'Disconnecting...' : 'Disconnect & Remove'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{userData && (
|
{userData && (
|
||||||
<View style={styles.userDataCard}>
|
<View style={styles.userDataCard}>
|
||||||
|
|
@ -1213,12 +1216,15 @@ const DebridIntegrationScreen = () => {
|
||||||
<Text style={styles.sectionText}>
|
<Text style={styles.sectionText}>
|
||||||
Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.
|
Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.subscribeButton}
|
style={styles.subscribeButton}
|
||||||
onPress={() => Linking.openURL('https://torbox.app/settings?section=integration-settings')}
|
onPress={() => Linking.openURL('https://torbox.app/settings?section=integration-settings')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<Text style={styles.subscribeButtonText}>Open Settings</Text>
|
<Text style={styles.subscribeButtonText}>Open Settings</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -1227,9 +1233,15 @@ const DebridIntegrationScreen = () => {
|
||||||
Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.
|
Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<TouchableOpacity onPress={() => Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')} style={styles.guideLink}>
|
<FocusableTouchableOpacity
|
||||||
|
onPress={() => Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')}
|
||||||
|
style={styles.guideLink}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
>
|
||||||
<Text style={styles.guideLinkText}>What is a Debrid Service?</Text>
|
<Text style={styles.guideLinkText}>What is a Debrid Service?</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
<View style={styles.inputContainer}>
|
||||||
<Text style={styles.label}>Torbox API Key</Text>
|
<Text style={styles.label}>Torbox API Key</Text>
|
||||||
|
|
@ -1245,24 +1257,33 @@ const DebridIntegrationScreen = () => {
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.connectButton, loading && styles.disabledButton]}
|
style={[styles.connectButton, loading && styles.disabledButton]}
|
||||||
onPress={handleConnect}
|
onPress={handleConnect}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<Text style={styles.connectButtonText}>
|
<Text style={styles.connectButtonText}>
|
||||||
{loading ? 'Connecting...' : 'Connect & Install'}
|
{loading ? 'Connecting...' : 'Connect & Install'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>Unlock Premium Speeds</Text>
|
<Text style={styles.sectionTitle}>Unlock Premium Speeds</Text>
|
||||||
<Text style={styles.sectionText}>
|
<Text style={styles.sectionText}>
|
||||||
Get a Torbox subscription to access cached high-quality streams with zero buffering.
|
Get a Torbox subscription to access cached high-quality streams with zero buffering.
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity style={styles.subscribeButton} onPress={openSubscription}>
|
<FocusableTouchableOpacity
|
||||||
|
style={styles.subscribeButton}
|
||||||
|
onPress={openSubscription}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
|
>
|
||||||
<Text style={styles.subscribeButtonText}>Get Subscription</Text>
|
<Text style={styles.subscribeButtonText}>Get Subscription</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1306,12 +1327,15 @@ const DebridIntegrationScreen = () => {
|
||||||
<Text style={styles.promoText}>
|
<Text style={styles.promoText}>
|
||||||
Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.
|
Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.promoButton}
|
style={styles.promoButton}
|
||||||
onPress={() => Linking.openURL('https://torbox.app/subscription?referral=493192f2-6403-440f-b414-768f72222ec7')}
|
onPress={() => Linking.openURL('https://torbox.app/subscription?referral=493192f2-6403-440f-b414-768f72222ec7')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<Text style={styles.promoButtonText}>Get TorBox Subscription</Text>
|
<Text style={styles.promoButtonText}>Get TorBox Subscription</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1320,13 +1344,17 @@ const DebridIntegrationScreen = () => {
|
||||||
<Text style={styles.configSectionTitle}>Debrid Service *</Text>
|
<Text style={styles.configSectionTitle}>Debrid Service *</Text>
|
||||||
<View style={styles.pickerContainer}>
|
<View style={styles.pickerContainer}>
|
||||||
{TORRENTIO_DEBRID_SERVICES.map((service: any) => (
|
{TORRENTIO_DEBRID_SERVICES.map((service: any) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={service.id}
|
key={service.id}
|
||||||
style={[
|
style={[
|
||||||
styles.pickerItem,
|
styles.pickerItem,
|
||||||
torrentioConfig.debridService === service.id && styles.pickerItemSelected
|
torrentioConfig.debridService === service.id && styles.pickerItemSelected
|
||||||
]}
|
]}
|
||||||
onPress={() => setTorrentioConfig(prev => ({ ...prev, debridService: service.id }))}
|
onPress={() => setTorrentioConfig(prev => ({ ...prev, debridService: service.id }))}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && torrentioConfig.debridService === service.id}
|
||||||
>
|
>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.pickerItemText,
|
styles.pickerItemText,
|
||||||
|
|
@ -1334,7 +1362,7 @@ const DebridIntegrationScreen = () => {
|
||||||
]}>
|
]}>
|
||||||
{service.name}
|
{service.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1355,9 +1383,12 @@ const DebridIntegrationScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Sorting - Accordion */}
|
{/* Sorting - Accordion */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.accordionHeader, expandedSections.sorting && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
style={[styles.accordionHeader, expandedSections.sorting && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||||
onPress={() => toggleSection('sorting')}
|
onPress={() => toggleSection('sorting')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.accordionHeaderText}>Sorting</Text>
|
<Text style={styles.accordionHeaderText}>Sorting</Text>
|
||||||
|
|
@ -1366,29 +1397,36 @@ const DebridIntegrationScreen = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Feather name={expandedSections.sorting ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
<Feather name={expandedSections.sorting ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{expandedSections.sorting && (
|
{expandedSections.sorting && (
|
||||||
<View style={styles.accordionContent}>
|
<View style={styles.accordionContent}>
|
||||||
<View style={styles.pickerContainer}>
|
<View style={styles.pickerContainer}>
|
||||||
{TORRENTIO_SORT_OPTIONS.map(option => (
|
{TORRENTIO_SORT_OPTIONS.map(option => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={option.id}
|
key={option.id}
|
||||||
style={[styles.pickerItem, torrentioConfig.sort === option.id && styles.pickerItemSelected]}
|
style={[styles.pickerItem, torrentioConfig.sort === option.id && styles.pickerItemSelected]}
|
||||||
onPress={() => setTorrentioConfig(prev => ({ ...prev, sort: option.id }))}
|
onPress={() => setTorrentioConfig(prev => ({ ...prev, sort: option.id }))}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && torrentioConfig.sort === option.id}
|
||||||
>
|
>
|
||||||
<Text style={[styles.pickerItemText, torrentioConfig.sort === option.id && styles.pickerItemTextSelected]}>
|
<Text style={[styles.pickerItemText, torrentioConfig.sort === option.id && styles.pickerItemTextSelected]}>
|
||||||
{option.name}
|
{option.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quality Filter - Accordion */}
|
{/* Quality Filter - Accordion */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.accordionHeader, expandedSections.qualityFilter && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
style={[styles.accordionHeader, expandedSections.qualityFilter && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||||
onPress={() => toggleSection('qualityFilter')}
|
onPress={() => toggleSection('qualityFilter')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.accordionHeaderText}>Exclude Qualities</Text>
|
<Text style={styles.accordionHeaderText}>Exclude Qualities</Text>
|
||||||
|
|
@ -1397,29 +1435,36 @@ const DebridIntegrationScreen = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Feather name={expandedSections.qualityFilter ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
<Feather name={expandedSections.qualityFilter ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{expandedSections.qualityFilter && (
|
{expandedSections.qualityFilter && (
|
||||||
<View style={styles.accordionContent}>
|
<View style={styles.accordionContent}>
|
||||||
<View style={styles.chipContainer}>
|
<View style={styles.chipContainer}>
|
||||||
{TORRENTIO_QUALITY_FILTERS.map(quality => (
|
{TORRENTIO_QUALITY_FILTERS.map(quality => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={quality.id}
|
key={quality.id}
|
||||||
style={[styles.chip, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipSelected]}
|
style={[styles.chip, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipSelected]}
|
||||||
onPress={() => toggleQualityFilter(quality.id)}
|
onPress={() => toggleQualityFilter(quality.id)}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={999}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && torrentioConfig.qualityFilter.includes(quality.id)}
|
||||||
>
|
>
|
||||||
<Text style={[styles.chipText, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipTextSelected]}>
|
<Text style={[styles.chipText, torrentioConfig.qualityFilter.includes(quality.id) && styles.chipTextSelected]}>
|
||||||
{quality.name}
|
{quality.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Priority Languages - Accordion */}
|
{/* Priority Languages - Accordion */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.accordionHeader, expandedSections.languages && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
style={[styles.accordionHeader, expandedSections.languages && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||||
onPress={() => toggleSection('languages')}
|
onPress={() => toggleSection('languages')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.accordionHeaderText}>Priority Languages</Text>
|
<Text style={styles.accordionHeaderText}>Priority Languages</Text>
|
||||||
|
|
@ -1428,29 +1473,36 @@ const DebridIntegrationScreen = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Feather name={expandedSections.languages ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
<Feather name={expandedSections.languages ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{expandedSections.languages && (
|
{expandedSections.languages && (
|
||||||
<View style={styles.accordionContent}>
|
<View style={styles.accordionContent}>
|
||||||
<View style={styles.chipContainer}>
|
<View style={styles.chipContainer}>
|
||||||
{TORRENTIO_LANGUAGES.map(lang => (
|
{TORRENTIO_LANGUAGES.map(lang => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={lang.id}
|
key={lang.id}
|
||||||
style={[styles.chip, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipSelected]}
|
style={[styles.chip, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipSelected]}
|
||||||
onPress={() => toggleLanguage(lang.id)}
|
onPress={() => toggleLanguage(lang.id)}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={999}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && torrentioConfig.priorityLanguages.includes(lang.id)}
|
||||||
>
|
>
|
||||||
<Text style={[styles.chipText, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipTextSelected]}>
|
<Text style={[styles.chipText, torrentioConfig.priorityLanguages.includes(lang.id) && styles.chipTextSelected]}>
|
||||||
{lang.name}
|
{lang.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Max Results - Accordion */}
|
{/* Max Results - Accordion */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.accordionHeader, expandedSections.maxResults && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
style={[styles.accordionHeader, expandedSections.maxResults && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||||
onPress={() => toggleSection('maxResults')}
|
onPress={() => toggleSection('maxResults')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.accordionHeaderText}>Max Results</Text>
|
<Text style={styles.accordionHeaderText}>Max Results</Text>
|
||||||
|
|
@ -1459,36 +1511,43 @@ const DebridIntegrationScreen = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Feather name={expandedSections.maxResults ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
<Feather name={expandedSections.maxResults ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{expandedSections.maxResults && (
|
{expandedSections.maxResults && (
|
||||||
<View style={styles.accordionContent}>
|
<View style={styles.accordionContent}>
|
||||||
<View style={styles.pickerContainer}>
|
<View style={styles.pickerContainer}>
|
||||||
{TORRENTIO_MAX_RESULTS.map(option => (
|
{TORRENTIO_MAX_RESULTS.map(option => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={option.id || 'all'}
|
key={option.id || 'all'}
|
||||||
style={[styles.pickerItem, torrentioConfig.maxResults === option.id && styles.pickerItemSelected]}
|
style={[styles.pickerItem, torrentioConfig.maxResults === option.id && styles.pickerItemSelected]}
|
||||||
onPress={() => setTorrentioConfig(prev => ({ ...prev, maxResults: option.id }))}
|
onPress={() => setTorrentioConfig(prev => ({ ...prev, maxResults: option.id }))}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && torrentioConfig.maxResults === option.id}
|
||||||
>
|
>
|
||||||
<Text style={[styles.pickerItemText, torrentioConfig.maxResults === option.id && styles.pickerItemTextSelected]}>
|
<Text style={[styles.pickerItemText, torrentioConfig.maxResults === option.id && styles.pickerItemTextSelected]}>
|
||||||
{option.name}
|
{option.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Additional Options - Accordion */}
|
{/* Additional Options - Accordion */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.accordionHeader, expandedSections.options && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
style={[styles.accordionHeader, expandedSections.options && { borderBottomLeftRadius: 0, borderBottomRightRadius: 0, marginBottom: 0 }]}
|
||||||
onPress={() => toggleSection('options')}
|
onPress={() => toggleSection('options')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text style={styles.accordionHeaderText}>Additional Options</Text>
|
<Text style={styles.accordionHeaderText}>Additional Options</Text>
|
||||||
<Text style={styles.accordionSubtext}>Catalog & download settings</Text>
|
<Text style={styles.accordionSubtext}>Catalog & download settings</Text>
|
||||||
</View>
|
</View>
|
||||||
<Feather name={expandedSections.options ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
<Feather name={expandedSections.options ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{expandedSections.options && (
|
{expandedSections.options && (
|
||||||
<View style={styles.accordionContent}>
|
<View style={styles.accordionContent}>
|
||||||
<View style={styles.switchRow}>
|
<View style={styles.switchRow}>
|
||||||
|
|
@ -1526,33 +1585,42 @@ const DebridIntegrationScreen = () => {
|
||||||
<View style={{ marginTop: 8 }}>
|
<View style={{ marginTop: 8 }}>
|
||||||
{torrentioConfig.isInstalled ? (
|
{torrentioConfig.isInstalled ? (
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.connectButton, torrentioLoading && styles.disabledButton]}
|
style={[styles.connectButton, torrentioLoading && styles.disabledButton]}
|
||||||
onPress={handleInstallTorrentio}
|
onPress={handleInstallTorrentio}
|
||||||
disabled={torrentioLoading}
|
disabled={torrentioLoading}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<Text style={styles.connectButtonText}>
|
<Text style={styles.connectButtonText}>
|
||||||
{torrentioLoading ? 'Updating...' : 'Update Configuration'}
|
{torrentioLoading ? 'Updating...' : 'Update Configuration'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.actionButton, styles.dangerButton, torrentioLoading && styles.disabledButton]}
|
style={[styles.actionButton, styles.dangerButton, torrentioLoading && styles.disabledButton]}
|
||||||
onPress={handleRemoveTorrentio}
|
onPress={handleRemoveTorrentio}
|
||||||
disabled={torrentioLoading}
|
disabled={torrentioLoading}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Remove Torrentio</Text>
|
<Text style={styles.buttonText}>Remove Torrentio</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.connectButton, torrentioLoading && styles.disabledButton]}
|
style={[styles.connectButton, torrentioLoading && styles.disabledButton]}
|
||||||
onPress={handleInstallTorrentio}
|
onPress={handleInstallTorrentio}
|
||||||
disabled={torrentioLoading}
|
disabled={torrentioLoading}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<Text style={styles.connectButtonText}>
|
<Text style={styles.connectButtonText}>
|
||||||
{torrentioLoading ? 'Installing...' : 'Install Torrentio'}
|
{torrentioLoading ? 'Installing...' : 'Install Torrentio'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -1578,33 +1646,45 @@ const DebridIntegrationScreen = () => {
|
||||||
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
|
<StatusBar barStyle="light-content" backgroundColor={colors.darkBackground} />
|
||||||
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={999}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Feather name="arrow-left" size={24} color={colors.white} />
|
<Feather name="arrow-left" size={24} color={colors.white} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>Debrid Integration</Text>
|
<Text style={styles.headerTitle}>Debrid Integration</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Tab Selector */}
|
{/* Tab Selector */}
|
||||||
<View style={styles.tabContainer}>
|
<View style={styles.tabContainer}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.tab, activeTab === 'torbox' && styles.activeTab]}
|
style={[styles.tab, activeTab === 'torbox' && styles.activeTab]}
|
||||||
onPress={() => setActiveTab('torbox')}
|
onPress={() => setActiveTab('torbox')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && activeTab === 'torbox'}
|
||||||
>
|
>
|
||||||
<Text style={[styles.tabText, activeTab === 'torbox' && styles.activeTabText]}>
|
<Text style={[styles.tabText, activeTab === 'torbox' && styles.activeTabText]}>
|
||||||
TorBox
|
TorBox
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.tab, activeTab === 'torrentio' && styles.activeTab]}
|
style={[styles.tab, activeTab === 'torrentio' && styles.activeTab]}
|
||||||
onPress={() => setActiveTab('torrentio')}
|
onPress={() => setActiveTab('torrentio')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && activeTab === 'torrentio'}
|
||||||
>
|
>
|
||||||
<Text style={[styles.tabText, activeTab === 'torrentio' && styles.activeTabText]}>
|
<Text style={[styles.tabText, activeTab === 'torrentio' && styles.activeTabText]}>
|
||||||
Torrentio
|
Torrentio
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
TouchableOpacity,
|
|
||||||
FlatList,
|
FlatList,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -35,6 +34,7 @@ import type { DownloadItem } from '../contexts/DownloadsContext';
|
||||||
import { useToast } from '../contexts/ToastContext';
|
import { useToast } from '../contexts/ToastContext';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
import ScreenHeader from '../components/common/ScreenHeader';
|
import ScreenHeader from '../components/common/ScreenHeader';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { height, width } = Dimensions.get('window');
|
const { height, width } = Dimensions.get('window');
|
||||||
const isTablet = width >= 768;
|
const isTablet = width >= 768;
|
||||||
|
|
@ -80,7 +80,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
|
||||||
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
Downloaded content will appear here for offline viewing
|
Downloaded content will appear here for offline viewing
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('Search');
|
navigation.navigate('Search');
|
||||||
|
|
@ -89,7 +89,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
|
||||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.background }]}>
|
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.background }]}>
|
||||||
Explore Content
|
Explore Content
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -204,7 +204,7 @@ const DownloadItemComponent: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.downloadItem, { backgroundColor: currentTheme.colors.elevation2 }]}
|
style={[styles.downloadItem, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||||
onPress={() => onPress(item)}
|
onPress={() => onPress(item)}
|
||||||
onLongPress={handleLongPress}
|
onLongPress={handleLongPress}
|
||||||
|
|
@ -314,7 +314,7 @@ const DownloadItemComponent: React.FC<{
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<View style={styles.actionContainer}>
|
<View style={styles.actionContainer}>
|
||||||
{getActionIcon() && (
|
{getActionIcon() && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||||
onPress={handleActionPress}
|
onPress={handleActionPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -324,10 +324,10 @@ const DownloadItemComponent: React.FC<{
|
||||||
size={20}
|
size={20}
|
||||||
color={currentTheme.colors.primary}
|
color={currentTheme.colors.primary}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
style={[styles.actionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||||
onPress={() => onRequestRemove(item)}
|
onPress={() => onRequestRemove(item)}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -337,9 +337,9 @@ const DownloadItemComponent: React.FC<{
|
||||||
size={20}
|
size={20}
|
||||||
color={currentTheme.colors.error}
|
color={currentTheme.colors.error}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -568,7 +568,7 @@ const DownloadsScreen: React.FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => (
|
const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={filter}
|
key={filter}
|
||||||
style={[
|
style={[
|
||||||
styles.filterButton,
|
styles.filterButton,
|
||||||
|
|
@ -612,7 +612,7 @@ const DownloadsScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -627,7 +627,7 @@ const DownloadsScreen: React.FC = () => {
|
||||||
<ScreenHeader
|
<ScreenHeader
|
||||||
title="Downloads"
|
title="Downloads"
|
||||||
rightActionComponent={
|
rightActionComponent={
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.helpButton}
|
style={styles.helpButton}
|
||||||
onPress={showDownloadHelp}
|
onPress={showDownloadHelp}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -637,7 +637,7 @@ const DownloadsScreen: React.FC = () => {
|
||||||
size={24}
|
size={24}
|
||||||
color={currentTheme.colors.mediumEmphasis}
|
color={currentTheme.colors.mediumEmphasis}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
}
|
}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
|
@ -77,6 +76,7 @@ import { useToast } from '../contexts/ToastContext';
|
||||||
import FirstTimeWelcome from '../components/FirstTimeWelcome';
|
import FirstTimeWelcome from '../components/FirstTimeWelcome';
|
||||||
import { HeaderVisibility } from '../contexts/HeaderVisibility';
|
import { HeaderVisibility } from '../contexts/HeaderVisibility';
|
||||||
import { useTrailer } from '../contexts/TrailerContext';
|
import { useTrailer } from '../contexts/TrailerContext';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
const CATALOG_SETTINGS_KEY = 'catalog_settings';
|
||||||
|
|
@ -799,15 +799,18 @@ const HomeScreen = () => {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View style={styles.loadMoreContainer}>
|
<View style={styles.loadMoreContainer}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.loadMoreButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.loadMoreButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={handleLoadMoreCatalogs}
|
onPress={handleLoadMoreCatalogs}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
|
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
|
||||||
<Text style={[styles.loadMoreText, { color: currentTheme.colors.white }]}>
|
<Text style={[styles.loadMoreText, { color: currentTheme.colors.white }]}>
|
||||||
Load More Catalogs
|
Load More Catalogs
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -829,13 +832,17 @@ const HomeScreen = () => {
|
||||||
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||||
No content available
|
No content available
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={() => navigation.navigate('Settings')}
|
onPress={() => navigation.navigate('Settings')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
||||||
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
useColorScheme,
|
useColorScheme,
|
||||||
useWindowDimensions,
|
useWindowDimensions,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
|
|
@ -38,6 +37,7 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
||||||
import { traktService, TraktService, TraktImages } from '../services/traktService';
|
import { traktService, TraktService, TraktImages } from '../services/traktService';
|
||||||
import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
|
import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
interface LibraryItem extends StreamingContent {
|
interface LibraryItem extends StreamingContent {
|
||||||
progress?: number;
|
progress?: number;
|
||||||
|
|
@ -121,10 +121,13 @@ const TraktItem = React.memo(({
|
||||||
}, [navigation, item.imdbId, item.type]);
|
}, [navigation, item.imdbId, item.type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.itemContainer, { width }]}
|
style={[styles.itemContainer, { width }]}
|
||||||
onPress={handlePress}
|
onPress={handlePress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="poster"
|
||||||
|
focusBorderRadius={12}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||||
|
|
@ -146,7 +149,7 @@ const TraktItem = React.memo(({
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -386,7 +389,7 @@ const LibraryScreen = () => {
|
||||||
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
}, [traktAuthenticated, watchedMovies, watchedShows, watchlistMovies, watchlistShows, collectionMovies, collectionShows, continueWatching, ratedContent]);
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: LibraryItem }) => (
|
const renderItem = ({ item }: { item: LibraryItem }) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.itemContainer, { width: itemWidth }]}
|
style={[styles.itemContainer, { width: itemWidth }]}
|
||||||
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
onPress={() => navigation.navigate('Metadata', { id: item.id, type: item.type })}
|
||||||
onLongPress={() => {
|
onLongPress={() => {
|
||||||
|
|
@ -394,6 +397,9 @@ const LibraryScreen = () => {
|
||||||
setMenuVisible(true);
|
setMenuVisible(true);
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="poster"
|
||||||
|
focusBorderRadius={12}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
<View style={[styles.posterContainer, { shadowColor: currentTheme.colors.black }]}>
|
||||||
|
|
@ -424,17 +430,20 @@ const LibraryScreen = () => {
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
|
const renderTraktCollectionFolder = ({ folder }: { folder: TraktFolder }) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.itemContainer, { width: itemWidth }]}
|
style={[styles.itemContainer, { width: itemWidth }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedTraktFolder(folder.id);
|
setSelectedTraktFolder(folder.id);
|
||||||
loadAllCollections();
|
loadAllCollections();
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="card"
|
||||||
|
focusBorderRadius={12}
|
||||||
>
|
>
|
||||||
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
|
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
<View style={styles.folderGradient}>
|
<View style={styles.folderGradient}>
|
||||||
|
|
@ -452,11 +461,11 @@ const LibraryScreen = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderTraktFolder = () => (
|
const renderTraktFolder = () => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.itemContainer, { width: itemWidth }]}
|
style={[styles.itemContainer, { width: itemWidth }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (!traktAuthenticated) {
|
if (!traktAuthenticated) {
|
||||||
|
|
@ -468,6 +477,9 @@ const LibraryScreen = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="card"
|
||||||
|
focusBorderRadius={12}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
|
<View style={[styles.posterContainer, styles.folderContainer, { shadowColor: currentTheme.colors.black, backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
|
|
@ -489,7 +501,7 @@ const LibraryScreen = () => {
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => {
|
const renderTraktItem = useCallback(({ item }: { item: TraktDisplayItem }) => {
|
||||||
|
|
@ -715,7 +727,7 @@ const LibraryScreen = () => {
|
||||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||||
Your Trakt collections will appear here once you start using Trakt
|
Your Trakt collections will appear here once you start using Trakt
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.exploreButton, {
|
style={[styles.exploreButton, {
|
||||||
backgroundColor: currentTheme.colors.primary,
|
backgroundColor: currentTheme.colors.primary,
|
||||||
shadowColor: currentTheme.colors.black
|
shadowColor: currentTheme.colors.black
|
||||||
|
|
@ -724,9 +736,13 @@ const LibraryScreen = () => {
|
||||||
loadAllCollections();
|
loadAllCollections();
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Load Collections</Text>
|
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Load Collections</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -756,7 +772,7 @@ const LibraryScreen = () => {
|
||||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||||
This collection is empty
|
This collection is empty
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.exploreButton, {
|
style={[styles.exploreButton, {
|
||||||
backgroundColor: currentTheme.colors.primary,
|
backgroundColor: currentTheme.colors.primary,
|
||||||
shadowColor: currentTheme.colors.black
|
shadowColor: currentTheme.colors.black
|
||||||
|
|
@ -765,9 +781,13 @@ const LibraryScreen = () => {
|
||||||
loadAllCollections();
|
loadAllCollections();
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Refresh</Text>
|
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Refresh</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -791,7 +811,7 @@ const LibraryScreen = () => {
|
||||||
const isActive = filter === filterType;
|
const isActive = filter === filterType;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.filterButton,
|
styles.filterButton,
|
||||||
isActive && { backgroundColor: currentTheme.colors.primary },
|
isActive && { backgroundColor: currentTheme.colors.primary },
|
||||||
|
|
@ -811,6 +831,10 @@ const LibraryScreen = () => {
|
||||||
setFilter(filterType);
|
setFilter(filterType);
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={18}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && isActive}
|
||||||
>
|
>
|
||||||
{filterType === 'trakt' ? (
|
{filterType === 'trakt' ? (
|
||||||
<View style={[styles.filterIcon, { justifyContent: 'center', alignItems: 'center' }]}>
|
<View style={[styles.filterIcon, { justifyContent: 'center', alignItems: 'center' }]}>
|
||||||
|
|
@ -833,7 +857,7 @@ const LibraryScreen = () => {
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -858,16 +882,20 @@ const LibraryScreen = () => {
|
||||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||||
{emptySubtitle}
|
{emptySubtitle}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.exploreButton, {
|
style={[styles.exploreButton, {
|
||||||
backgroundColor: currentTheme.colors.primary,
|
backgroundColor: currentTheme.colors.primary,
|
||||||
shadowColor: currentTheme.colors.black
|
shadowColor: currentTheme.colors.black
|
||||||
}]}
|
}]}
|
||||||
onPress={() => navigation.navigate('Search')}
|
onPress={() => navigation.navigate('Search')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Find something to watch</Text>
|
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Find something to watch</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
StatusBar,
|
StatusBar,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
TouchableOpacity,
|
|
||||||
InteractionManager,
|
InteractionManager,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
Platform,
|
Platform,
|
||||||
|
|
@ -64,6 +63,7 @@ import { useWatchProgress } from '../hooks/useWatchProgress';
|
||||||
import { TraktService, TraktPlaybackItem } from '../services/traktService';
|
import { TraktService, TraktPlaybackItem } from '../services/traktService';
|
||||||
import { tmdbService } from '../services/tmdbService';
|
import { tmdbService } from '../services/tmdbService';
|
||||||
import { catalogService } from '../services/catalogService';
|
import { catalogService } from '../services/catalogService';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { height } = Dimensions.get('window');
|
const { height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -865,21 +865,28 @@ const MetadataScreen: React.FC = () => {
|
||||||
{metadataError}
|
{metadataError}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={loadMetadata}
|
onPress={loadMetadata}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="refresh" size={20} color={currentTheme.colors.white} style={{ marginRight: 8 }} />
|
<MaterialIcons name="refresh" size={20} color={currentTheme.colors.white} style={{ marginRight: 8 }} />
|
||||||
<Text style={styles.retryButtonText}>Try Again</Text>
|
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.backButton, { borderColor: currentTheme.colors.primary }]}
|
style={[styles.backButton, { borderColor: currentTheme.colors.primary }]}
|
||||||
onPress={handleBack}
|
onPress={handleBack}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text>
|
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
@ -1240,7 +1247,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
{/* Backdrop Gallery section - shown after movie details for movies when TMDB ID is available and enrichment is enabled */}
|
{/* Backdrop Gallery section - shown after movie details for movies when TMDB ID is available and enrichment is enabled */}
|
||||||
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length === 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
|
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length === 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
|
||||||
<View style={styles.backdropGalleryContainer}>
|
<View style={styles.backdropGalleryContainer}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backdropGalleryButton}
|
style={styles.backdropGalleryButton}
|
||||||
onPress={() => navigation.navigate('BackdropGallery' as any, {
|
onPress={() => navigation.navigate('BackdropGallery' as any, {
|
||||||
tmdbId: metadata.tmdbId,
|
tmdbId: metadata.tmdbId,
|
||||||
|
|
@ -1248,10 +1255,13 @@ const MetadataScreen: React.FC = () => {
|
||||||
title: metadata.name || 'Gallery'
|
title: metadata.name || 'Gallery'
|
||||||
})}
|
})}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
|
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
|
||||||
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
|
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -1381,7 +1391,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
{/* Backdrop Gallery section - shown after show details for TV shows when TMDB ID is available and enrichment is enabled */}
|
{/* Backdrop Gallery section - shown after show details for TV shows when TMDB ID is available and enrichment is enabled */}
|
||||||
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
|
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.tmdbId && settings.enrichMetadataWithTMDB && (
|
||||||
<View style={styles.backdropGalleryContainer}>
|
<View style={styles.backdropGalleryContainer}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backdropGalleryButton}
|
style={styles.backdropGalleryButton}
|
||||||
onPress={() => navigation.navigate('BackdropGallery' as any, {
|
onPress={() => navigation.navigate('BackdropGallery' as any, {
|
||||||
tmdbId: metadata.tmdbId,
|
tmdbId: metadata.tmdbId,
|
||||||
|
|
@ -1389,10 +1399,13 @@ const MetadataScreen: React.FC = () => {
|
||||||
title: metadata.name || 'Gallery'
|
title: metadata.name || 'Gallery'
|
||||||
})}
|
})}
|
||||||
focusable={Platform.isTV}
|
focusable={Platform.isTV}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={16}
|
||||||
>
|
>
|
||||||
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
|
<Text style={[styles.backdropGalleryText, { color: currentTheme.colors.highEmphasis }]}>Backdrop Gallery</Text>
|
||||||
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
|
<MaterialIcons name="chevron-right" size={24} color={currentTheme.colors.highEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import {
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
TouchableOpacity,
|
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Platform,
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
@ -25,6 +24,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -281,9 +281,9 @@ const OnboardingScreen = () => {
|
||||||
entering={FadeIn.delay(300).duration(600)}
|
entering={FadeIn.delay(300).duration(600)}
|
||||||
style={styles.header}
|
style={styles.header}
|
||||||
>
|
>
|
||||||
<TouchableOpacity onPress={handleSkip} style={styles.skipButton}>
|
<FocusableTouchableOpacity onPress={handleSkip} style={styles.skipButton}>
|
||||||
<Text style={styles.skipText}>Skip</Text>
|
<Text style={styles.skipText}>Skip</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{/* Smooth Progress Bar */}
|
{/* Smooth Progress Bar */}
|
||||||
<View style={styles.progressContainer}>
|
<View style={styles.progressContainer}>
|
||||||
|
|
@ -322,7 +322,7 @@ const OnboardingScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Animated Button */}
|
{/* Animated Button */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={handleNext}
|
onPress={handleNext}
|
||||||
onPressIn={handlePressIn}
|
onPressIn={handlePressIn}
|
||||||
onPressOut={handlePressOut}
|
onPressOut={handlePressOut}
|
||||||
|
|
@ -333,7 +333,7 @@ const OnboardingScreen = () => {
|
||||||
{currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Continue'}
|
{currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Continue'}
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
Platform,
|
Platform,
|
||||||
TouchableOpacity,
|
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Switch,
|
Switch,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
@ -15,6 +14,7 @@ import { useSettings, AppSettings } from '../hooks/useSettings';
|
||||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -87,7 +87,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -173,7 +173,7 @@ const PlayerSettingsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={handleBack}
|
onPress={handleBack}
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -186,7 +186,7 @@ const PlayerSettingsScreen: React.FC = () => {
|
||||||
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
||||||
Settings
|
Settings
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
{/* Empty for now, but ready for future actions */}
|
{/* Empty for now, but ready for future actions */}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
Switch,
|
Switch,
|
||||||
TextInput,
|
TextInput,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
|
@ -25,6 +24,7 @@ import { useSettings } from '../hooks/useSettings';
|
||||||
import { localScraperService, pluginService, ScraperInfo, RepositoryInfo } from '../services/pluginService';
|
import { localScraperService, pluginService, ScraperInfo, RepositoryInfo } from '../services/pluginService';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get('window');
|
const { width: screenWidth } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -773,23 +773,23 @@ const CollapsibleSection: React.FC<{
|
||||||
styles: any;
|
styles: any;
|
||||||
}> = ({ title, children, isExpanded, onToggle, colors, styles }) => (
|
}> = ({ title, children, isExpanded, onToggle, colors, styles }) => (
|
||||||
<View style={styles.collapsibleSection}>
|
<View style={styles.collapsibleSection}>
|
||||||
<TouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
|
<FocusableTouchableOpacity style={styles.collapsibleHeader} onPress={onToggle}>
|
||||||
<Text style={styles.collapsibleTitle}>{title}</Text>
|
<Text style={styles.collapsibleTitle}>{title}</Text>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
name={isExpanded ? "chevron-up" : "chevron-down"}
|
name={isExpanded ? "chevron-up" : "chevron-down"}
|
||||||
size={20}
|
size={20}
|
||||||
color={colors.mediumGray}
|
color={colors.mediumGray}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
{isExpanded && <View style={styles.collapsibleContent}>{children}</View>}
|
{isExpanded && <View style={styles.collapsibleContent}>{children}</View>}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Helper component for info tooltips
|
// Helper component for info tooltips
|
||||||
const InfoTooltip: React.FC<{ text: string; colors: any }> = ({ text, colors }) => (
|
const InfoTooltip: React.FC<{ text: string; colors: any }> = ({ text, colors }) => (
|
||||||
<TouchableOpacity style={{ marginLeft: 8 }}>
|
<View style={{ marginLeft: 8 }}>
|
||||||
<Ionicons name="information-circle-outline" size={16} color={colors.mediumGray} />
|
<Ionicons name="information-circle-outline" size={16} color={colors.mediumGray} />
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Helper component for status badges
|
// Helper component for status badges
|
||||||
|
|
@ -1361,22 +1361,22 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
<Ionicons name="arrow-back" size={24} color={colors.primary} />
|
<Ionicons name="arrow-back" size={24} color={colors.primary} />
|
||||||
<Text style={styles.backText}>Settings</Text>
|
<Text style={styles.backText}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
{/* Help Button */}
|
{/* Help Button */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.headerButton}
|
style={styles.headerButton}
|
||||||
onPress={() => setShowHelpModal(true)}
|
onPress={() => setShowHelpModal(true)}
|
||||||
>
|
>
|
||||||
<Ionicons name="help-circle-outline" size={20} color={colors.primary} />
|
<Ionicons name="help-circle-outline" size={20} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -1485,7 +1485,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.repositoryActions}>
|
<View style={styles.repositoryActions}>
|
||||||
{repo.id !== currentRepositoryId && (
|
{repo.id !== currentRepositoryId && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonPrimary]}
|
style={[styles.repositoryActionButton, styles.repositoryActionButtonPrimary]}
|
||||||
onPress={() => handleSwitchRepository(repo.id)}
|
onPress={() => handleSwitchRepository(repo.id)}
|
||||||
disabled={switchingRepository === repo.id}
|
disabled={switchingRepository === repo.id}
|
||||||
|
|
@ -1495,9 +1495,9 @@ const PluginsScreen: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.repositoryActionButtonText}>Switch</Text>
|
<Text style={styles.repositoryActionButtonText}>Switch</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonSecondary]}
|
style={[styles.repositoryActionButton, styles.repositoryActionButtonSecondary]}
|
||||||
onPress={() => handleRefreshRepository()}
|
onPress={() => handleRefreshRepository()}
|
||||||
disabled={isRefreshing || switchingRepository !== null}
|
disabled={isRefreshing || switchingRepository !== null}
|
||||||
|
|
@ -1507,14 +1507,14 @@ const PluginsScreen: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.repositoryActionButtonText}>Refresh</Text>
|
<Text style={styles.repositoryActionButtonText}>Refresh</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.repositoryActionButton, styles.repositoryActionButtonDanger]}
|
style={[styles.repositoryActionButton, styles.repositoryActionButtonDanger]}
|
||||||
onPress={() => handleRemoveRepository(repo.id)}
|
onPress={() => handleRemoveRepository(repo.id)}
|
||||||
disabled={switchingRepository !== null}
|
disabled={switchingRepository !== null}
|
||||||
>
|
>
|
||||||
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
<Text style={styles.repositoryActionButtonText}>Remove</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
|
|
@ -1523,13 +1523,13 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
|
|
||||||
{/* Add Repository Button */}
|
{/* Add Repository Button */}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.button, styles.primaryButton, { marginTop: 16 }]}
|
style={[styles.button, styles.primaryButton, { marginTop: 16 }]}
|
||||||
onPress={() => setShowAddRepositoryModal(true)}
|
onPress={() => setShowAddRepositoryModal(true)}
|
||||||
disabled={!settings.enableLocalScrapers || switchingRepository !== null}
|
disabled={!settings.enableLocalScrapers || switchingRepository !== null}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Add New Repository</Text>
|
<Text style={styles.buttonText}>Add New Repository</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
{/* Available Plugins */}
|
{/* Available Plugins */}
|
||||||
|
|
@ -1553,16 +1553,16 @@ const PluginsScreen: React.FC = () => {
|
||||||
placeholderTextColor={colors.mediumGray}
|
placeholderTextColor={colors.mediumGray}
|
||||||
/>
|
/>
|
||||||
{searchQuery.length > 0 && (
|
{searchQuery.length > 0 && (
|
||||||
<TouchableOpacity onPress={() => setSearchQuery('')}>
|
<FocusableTouchableOpacity onPress={() => setSearchQuery('')}>
|
||||||
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
<Ionicons name="close-circle" size={20} color={colors.mediumGray} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Filter Chips */}
|
{/* Filter Chips */}
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
{['all', 'movie', 'tv'].map((filter) => (
|
{['all', 'movie', 'tv'].map((filter) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={filter}
|
key={filter}
|
||||||
style={[
|
style={[
|
||||||
styles.filterChip,
|
styles.filterChip,
|
||||||
|
|
@ -1576,27 +1576,27 @@ const PluginsScreen: React.FC = () => {
|
||||||
]}>
|
]}>
|
||||||
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
{filter === 'all' ? 'All' : filter === 'movie' ? 'Movies' : 'TV Shows'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Bulk Actions */}
|
{/* Bulk Actions */}
|
||||||
{filteredScrapers.length > 0 && (
|
{filteredScrapers.length > 0 && (
|
||||||
<View style={styles.bulkActionsContainer}>
|
<View style={styles.bulkActionsContainer}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.bulkActionButton, styles.bulkActionButtonEnabled]}
|
style={[styles.bulkActionButton, styles.bulkActionButtonEnabled]}
|
||||||
onPress={() => handleBulkToggle(true)}
|
onPress={() => handleBulkToggle(true)}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
>
|
>
|
||||||
<Text style={[styles.bulkActionButtonText, { color: '#34C759' }]}>Enable All</Text>
|
<Text style={[styles.bulkActionButtonText, { color: '#34C759' }]}>Enable All</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.bulkActionButton, styles.bulkActionButtonDisabled]}
|
style={[styles.bulkActionButton, styles.bulkActionButtonDisabled]}
|
||||||
onPress={() => handleBulkToggle(false)}
|
onPress={() => handleBulkToggle(false)}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
>
|
>
|
||||||
<Text style={[styles.bulkActionButtonText, { color: colors.mediumGray }]}>Disable All</Text>
|
<Text style={[styles.bulkActionButtonText, { color: colors.mediumGray }]}>Disable All</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -1620,12 +1620,12 @@ const PluginsScreen: React.FC = () => {
|
||||||
}
|
}
|
||||||
</Text>
|
</Text>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.button, styles.secondaryButton]}
|
style={[styles.button, styles.secondaryButton]}
|
||||||
onPress={() => setSearchQuery('')}
|
onPress={() => setSearchQuery('')}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
<Text style={styles.secondaryButtonText}>Clear Search</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -1713,14 +1713,14 @@ const PluginsScreen: React.FC = () => {
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
/>
|
/>
|
||||||
{showboxSavedToken.length > 0 && (
|
{showboxSavedToken.length > 0 && (
|
||||||
<TouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
|
<FocusableTouchableOpacity onPress={() => setShowboxTokenVisible(v => !v)} accessibilityRole="button" accessibilityLabel={showboxTokenVisible ? 'Hide token' : 'Show token'} style={{ marginLeft: 10 }}>
|
||||||
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
|
<Ionicons name={showboxTokenVisible ? 'eye-off' : 'eye'} size={18} color={colors.primary} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.buttonRow}>
|
<View style={styles.buttonRow}>
|
||||||
{showboxUiToken !== showboxSavedToken && (
|
{showboxUiToken !== showboxSavedToken && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.button, styles.primaryButton]}
|
style={[styles.button, styles.primaryButton]}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (showboxScraperId) {
|
if (showboxScraperId) {
|
||||||
|
|
@ -1731,9 +1731,9 @@ const PluginsScreen: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Save</Text>
|
<Text style={styles.buttonText}>Save</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.button, styles.secondaryButton]}
|
style={[styles.button, styles.secondaryButton]}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
setShowboxUiToken('');
|
setShowboxUiToken('');
|
||||||
|
|
@ -1744,7 +1744,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.secondaryButtonText}>Clear</Text>
|
<Text style={styles.secondaryButtonText}>Clear</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1849,7 +1849,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
{qualityOptions.map((quality) => {
|
{qualityOptions.map((quality) => {
|
||||||
const isExcluded = (settings.excludedQualities || []).includes(quality);
|
const isExcluded = (settings.excludedQualities || []).includes(quality);
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={quality}
|
key={quality}
|
||||||
style={[
|
style={[
|
||||||
styles.qualityChip,
|
styles.qualityChip,
|
||||||
|
|
@ -1866,7 +1866,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
]}>
|
]}>
|
||||||
{isExcluded ? '✕ ' : ''}{quality}
|
{isExcluded ? '✕ ' : ''}{quality}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1898,7 +1898,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
{languageOptions.map((language) => {
|
{languageOptions.map((language) => {
|
||||||
const isExcluded = (settings.excludedLanguages || []).includes(language);
|
const isExcluded = (settings.excludedLanguages || []).includes(language);
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={language}
|
key={language}
|
||||||
style={[
|
style={[
|
||||||
styles.qualityChip,
|
styles.qualityChip,
|
||||||
|
|
@ -1915,7 +1915,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
]}>
|
]}>
|
||||||
{isExcluded ? '✕ ' : ''}{language}
|
{isExcluded ? '✕ ' : ''}{language}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -1964,12 +1964,12 @@ const PluginsScreen: React.FC = () => {
|
||||||
<Text style={styles.modalText}>
|
<Text style={styles.modalText}>
|
||||||
4. <Text style={{ fontWeight: '600' }}>Enable Scrapers</Text> - Turn on the scrapers you want to use for streaming
|
4. <Text style={{ fontWeight: '600' }}>Enable Scrapers</Text> - Turn on the scrapers you want to use for streaming
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.modalButton}
|
style={styles.modalButton}
|
||||||
onPress={() => setShowHelpModal(false)}
|
onPress={() => setShowHelpModal(false)}
|
||||||
>
|
>
|
||||||
<Text style={styles.modalButtonText}>Got it!</Text>
|
<Text style={styles.modalButtonText}>Got it!</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
@ -2011,7 +2011,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<View style={styles.compactActions}>
|
<View style={styles.compactActions}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.compactButton, styles.cancelButton]}
|
style={[styles.compactButton, styles.cancelButton]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setShowAddRepositoryModal(false);
|
setShowAddRepositoryModal(false);
|
||||||
|
|
@ -2019,9 +2019,9 @@ const PluginsScreen: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
style={[styles.compactButton, styles.addButton, (!newRepositoryUrl.trim() || isLoading) && styles.disabledButton]}
|
||||||
onPress={handleAddRepository}
|
onPress={handleAddRepository}
|
||||||
disabled={!newRepositoryUrl.trim() || isLoading}
|
disabled={!newRepositoryUrl.trim() || isLoading}
|
||||||
|
|
@ -2031,7 +2031,7 @@ const PluginsScreen: React.FC = () => {
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.addButtonText}>Add</Text>
|
<Text style={styles.addButtonText}>Add</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
FlatList,
|
FlatList,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Platform,
|
Platform,
|
||||||
|
|
@ -17,6 +16,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { useTraktContext } from '../contexts/TraktContext';
|
import { useTraktContext } from '../contexts/TraktContext';
|
||||||
import { mmkvStorage } from '../services/mmkvStorage';
|
import { mmkvStorage } from '../services/mmkvStorage';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||||
const PROFILE_STORAGE_KEY = 'user_profiles';
|
const PROFILE_STORAGE_KEY = 'user_profiles';
|
||||||
|
|
@ -183,7 +183,7 @@ const ProfilesScreen: React.FC = () => {
|
||||||
|
|
||||||
const renderItem = ({ item }: { item: Profile }) => (
|
const renderItem = ({ item }: { item: Profile }) => (
|
||||||
<View style={styles.profileItem}>
|
<View style={styles.profileItem}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.profileContent,
|
styles.profileContent,
|
||||||
item.isActive && {
|
item.isActive && {
|
||||||
|
|
@ -211,14 +211,14 @@ const ProfilesScreen: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
{!item.isActive && (
|
{!item.isActive && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.deleteButton}
|
style={styles.deleteButton}
|
||||||
onPress={() => handleDeleteProfile(item.id)}
|
onPress={() => handleDeleteProfile(item.id)}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="delete" size={24} color={currentTheme.colors.error} />
|
<MaterialIcons name="delete" size={24} color={currentTheme.colors.error} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -227,7 +227,7 @@ const ProfilesScreen: React.FC = () => {
|
||||||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||||||
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={handleBack}
|
onPress={handleBack}
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
|
@ -237,7 +237,7 @@ const ProfilesScreen: React.FC = () => {
|
||||||
size={24}
|
size={24}
|
||||||
color={currentTheme.colors.text}
|
color={currentTheme.colors.text}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.headerTitle,
|
styles.headerTitle,
|
||||||
|
|
@ -260,7 +260,7 @@ const ProfilesScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
ListFooterComponent={
|
ListFooterComponent={
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.addButton,
|
styles.addButton,
|
||||||
{ backgroundColor: currentTheme.colors.elevation2 }
|
{ backgroundColor: currentTheme.colors.elevation2 }
|
||||||
|
|
@ -271,7 +271,7 @@ const ProfilesScreen: React.FC = () => {
|
||||||
<Text style={[styles.addButtonText, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.addButtonText, { color: currentTheme.colors.text }]}>
|
||||||
Add New Profile
|
Add New Profile
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -307,7 +307,7 @@ const ProfilesScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.modalButtons}>
|
<View style={styles.modalButtons}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.modalButton, styles.cancelButton]}
|
style={[styles.modalButton, styles.cancelButton]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setNewProfileName('');
|
setNewProfileName('');
|
||||||
|
|
@ -315,8 +315,8 @@ const ProfilesScreen: React.FC = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text>
|
<Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.modalButton,
|
styles.modalButton,
|
||||||
styles.createButton,
|
styles.createButton,
|
||||||
|
|
@ -325,7 +325,7 @@ const ProfilesScreen: React.FC = () => {
|
||||||
onPress={handleAddProfile}
|
onPress={handleAddProfile}
|
||||||
>
|
>
|
||||||
<Text style={{ color: '#fff' }}>Create</Text>
|
<Text style={{ color: '#fff' }}>Create</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TextInput,
|
TextInput,
|
||||||
FlatList,
|
FlatList,
|
||||||
TouchableOpacity,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
useColorScheme,
|
useColorScheme,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
|
|
@ -44,6 +43,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import LoadingSpinner from '../components/common/LoadingSpinner';
|
import LoadingSpinner from '../components/common/LoadingSpinner';
|
||||||
import ScreenHeader from '../components/common/ScreenHeader';
|
import ScreenHeader from '../components/common/ScreenHeader';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ const MAX_RECENT_SEARCHES = 10;
|
||||||
|
|
||||||
const PLACEHOLDER_POSTER = 'https://placehold.co/300x450/222222/CCCCCC?text=No+Poster';
|
const PLACEHOLDER_POSTER = 'https://placehold.co/300x450/222222/CCCCCC?text=No+Poster';
|
||||||
|
|
||||||
const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);
|
// NOTE: AnimatedTouchable was unused; keep focus wrapper for TV instead.
|
||||||
|
|
||||||
const SkeletonLoader = () => {
|
const SkeletonLoader = () => {
|
||||||
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
const pulseAnim = React.useRef(new RNAnimated.Value(0)).current;
|
||||||
|
|
@ -570,24 +570,29 @@ const SearchScreen = () => {
|
||||||
Recent Searches
|
Recent Searches
|
||||||
</Text>
|
</Text>
|
||||||
{recentSearches.map((search, index) => (
|
{recentSearches.map((search, index) => (
|
||||||
<TouchableOpacity
|
<View key={index} style={styles.recentSearchItem}>
|
||||||
key={index}
|
|
||||||
style={styles.recentSearchItem}
|
|
||||||
onPress={() => {
|
|
||||||
setQuery(search);
|
|
||||||
Keyboard.dismiss();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="history"
|
name="history"
|
||||||
size={20}
|
size={20}
|
||||||
color={currentTheme.colors.lightGray}
|
color={currentTheme.colors.lightGray}
|
||||||
style={styles.recentSearchIcon}
|
style={styles.recentSearchIcon}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.recentSearchText, { color: currentTheme.colors.white }]}>
|
<FocusableTouchableOpacity
|
||||||
{search}
|
style={{ flex: 1 }}
|
||||||
</Text>
|
onPress={() => {
|
||||||
<TouchableOpacity
|
setQuery(search);
|
||||||
|
Keyboard.dismiss();
|
||||||
|
}}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
>
|
||||||
|
<Text style={[styles.recentSearchText, { color: currentTheme.colors.white }]}>
|
||||||
|
{search}
|
||||||
|
</Text>
|
||||||
|
</FocusableTouchableOpacity>
|
||||||
|
<FocusableTouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const newRecentSearches = [...recentSearches];
|
const newRecentSearches = [...recentSearches];
|
||||||
newRecentSearches.splice(index, 1);
|
newRecentSearches.splice(index, 1);
|
||||||
|
|
@ -596,10 +601,15 @@ const SearchScreen = () => {
|
||||||
}}
|
}}
|
||||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
style={styles.recentSearchDeleteButton}
|
style={styles.recentSearchDeleteButton}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={12}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && index === 0}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="close" size={16} color={currentTheme.colors.lightGray} />
|
<MaterialIcons name="close" size={16} color={currentTheme.colors.lightGray} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -651,7 +661,7 @@ const SearchScreen = () => {
|
||||||
}, [item.id, item.type]);
|
}, [item.id, item.type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.horizontalItem, { width: itemWidth }]}
|
style={[styles.horizontalItem, { width: itemWidth }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('Metadata', { id: item.id, type: item.type });
|
navigation.navigate('Metadata', { id: item.id, type: item.type });
|
||||||
|
|
@ -663,6 +673,9 @@ const SearchScreen = () => {
|
||||||
}}
|
}}
|
||||||
delayLongPress={300}
|
delayLongPress={300}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="poster"
|
||||||
|
focusBorderRadius={12}
|
||||||
>
|
>
|
||||||
<View style={[styles.horizontalItemPosterContainer, {
|
<View style={[styles.horizontalItemPosterContainer, {
|
||||||
width: itemWidth,
|
width: itemWidth,
|
||||||
|
|
@ -716,7 +729,7 @@ const SearchScreen = () => {
|
||||||
{item.year}
|
{item.year}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -934,17 +947,21 @@ const SearchScreen = () => {
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
{query.length > 0 && (
|
{query.length > 0 && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={handleClearSearch}
|
onPress={handleClearSearch}
|
||||||
style={styles.clearButton}
|
style={styles.clearButton}
|
||||||
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="icon"
|
||||||
|
focusBorderRadius={999}
|
||||||
>
|
>
|
||||||
<MaterialIcons
|
<MaterialIcons
|
||||||
name="close"
|
name="close"
|
||||||
size={20}
|
size={20}
|
||||||
color={currentTheme.colors.lightGray}
|
color={currentTheme.colors.lightGray}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
Switch,
|
Switch,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
|
|
@ -56,6 +55,7 @@ import TraktIcon from '../components/icons/TraktIcon';
|
||||||
import TMDBIcon from '../components/icons/TMDBIcon';
|
import TMDBIcon from '../components/icons/TMDBIcon';
|
||||||
import MDBListIcon from '../components/icons/MDBListIcon';
|
import MDBListIcon from '../components/icons/MDBListIcon';
|
||||||
import { campaignService } from '../services/campaignService';
|
import { campaignService } from '../services/campaignService';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width, height } = Dimensions.get('window');
|
const { width, height } = Dimensions.get('window');
|
||||||
const isTablet = width >= 768;
|
const isTablet = width >= 768;
|
||||||
|
|
@ -144,7 +144,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
activeOpacity={0.6}
|
activeOpacity={0.6}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -153,6 +153,9 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
||||||
{ borderBottomColor: currentTheme.colors.elevation2 },
|
{ borderBottomColor: currentTheme.colors.elevation2 },
|
||||||
isTablet && styles.tabletSettingItem
|
isTablet && styles.tabletSettingItem
|
||||||
]}
|
]}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={isTablet ? 18 : 16}
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.settingIconContainer,
|
styles.settingIconContainer,
|
||||||
|
|
@ -201,7 +204,7 @@ const SettingItem: React.FC<SettingItemProps> = ({
|
||||||
{renderControl()}
|
{renderControl()}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -237,7 +240,7 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
|
||||||
|
|
||||||
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.sidebarContent} showsVerticalScrollIndicator={false}>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={category.id}
|
key={category.id}
|
||||||
style={[
|
style={[
|
||||||
styles.sidebarItem,
|
styles.sidebarItem,
|
||||||
|
|
@ -248,6 +251,10 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
|
||||||
]}
|
]}
|
||||||
onPress={() => onCategorySelect(category.id)}
|
onPress={() => onCategorySelect(category.id)}
|
||||||
activeOpacity={0.6}
|
activeOpacity={0.6}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={14}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && selectedCategory === category.id}
|
||||||
>
|
>
|
||||||
<View style={[
|
<View style={[
|
||||||
styles.sidebarItemIconContainer,
|
styles.sidebarItemIconContainer,
|
||||||
|
|
@ -278,7 +285,7 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedCategory, onCategorySelect, c
|
||||||
]}>
|
]}>
|
||||||
{category.title}
|
{category.title}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -959,7 +966,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={styles.discordContainer}>
|
<View style={styles.discordContainer}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
|
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const url = 'https://ko-fi.com/tapframe';
|
const url = 'https://ko-fi.com/tapframe';
|
||||||
|
|
@ -973,19 +980,25 @@ const SettingsScreen: React.FC = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={require('../../assets/support_me_on_kofi_red.png')}
|
source={require('../../assets/support_me_on_kofi_red.png')}
|
||||||
style={styles.kofiImage}
|
style={styles.kofiImage}
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||||
onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')}
|
onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<View style={styles.discordButtonContent}>
|
<View style={styles.discordButtonContent}>
|
||||||
<FastImage
|
<FastImage
|
||||||
|
|
@ -997,12 +1010,15 @@ const SettingsScreen: React.FC = () => {
|
||||||
Discord
|
Discord
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]}
|
style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]}
|
||||||
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
|
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<View style={styles.discordButtonContent}>
|
<View style={styles.discordButtonContent}>
|
||||||
<FastImage
|
<FastImage
|
||||||
|
|
@ -1014,7 +1030,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
Reddit
|
Reddit
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -1093,7 +1109,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Support & Community Buttons */}
|
{/* Support & Community Buttons */}
|
||||||
<View style={styles.discordContainer}>
|
<View style={styles.discordContainer}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
|
style={[styles.discordButton, { backgroundColor: 'transparent', paddingVertical: 0, paddingHorizontal: 0, marginBottom: 8 }]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const url = 'https://ko-fi.com/tapframe';
|
const url = 'https://ko-fi.com/tapframe';
|
||||||
|
|
@ -1107,19 +1123,25 @@ const SettingsScreen: React.FC = () => {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={require('../../assets/support_me_on_kofi_red.png')}
|
source={require('../../assets/support_me_on_kofi_red.png')}
|
||||||
style={styles.kofiImage}
|
style={styles.kofiImage}
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
|
<View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
style={[styles.discordButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||||
onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')}
|
onPress={() => Linking.openURL('https://discord.gg/6w8dr3TSDN')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<View style={styles.discordButtonContent}>
|
<View style={styles.discordButtonContent}>
|
||||||
<FastImage
|
<FastImage
|
||||||
|
|
@ -1131,12 +1153,15 @@ const SettingsScreen: React.FC = () => {
|
||||||
Discord
|
Discord
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]}
|
style={[styles.discordButton, { backgroundColor: '#FF4500' + '15' }]}
|
||||||
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
|
onPress={() => Linking.openURL('https://www.reddit.com/r/Nuvio/')}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={14}
|
||||||
>
|
>
|
||||||
<View style={styles.discordButtonContent}>
|
<View style={styles.discordButtonContent}>
|
||||||
<FastImage
|
<FastImage
|
||||||
|
|
@ -1148,7 +1173,7 @@ const SettingsScreen: React.FC = () => {
|
||||||
Reddit
|
Reddit
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
TouchableOpacity,
|
|
||||||
Platform,
|
Platform,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
@ -34,6 +33,7 @@ import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
import Animated, { FadeIn, SlideInRight, withTiming, useAnimatedStyle, withSpring } from 'react-native-reanimated';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
type RootStackParamList = {
|
type RootStackParamList = {
|
||||||
ShowRatings: { showId: number };
|
ShowRatings: { showId: number };
|
||||||
|
|
@ -140,7 +140,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
|
||||||
{['tmdb', 'imdb', 'tvmaze'].map((source) => {
|
{['tmdb', 'imdb', 'tvmaze'].map((source) => {
|
||||||
const isActive = ratingSource === source;
|
const isActive = ratingSource === source;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={source}
|
key={source}
|
||||||
style={[
|
style={[
|
||||||
styles.sourceButton,
|
styles.sourceButton,
|
||||||
|
|
@ -148,6 +148,10 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
|
||||||
isActive && { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary }
|
isActive && { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary }
|
||||||
]}
|
]}
|
||||||
onPress={() => setRatingSource(source as RatingSource)}
|
onPress={() => setRatingSource(source as RatingSource)}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="pill"
|
||||||
|
focusBorderRadius={8}
|
||||||
|
hasTVPreferredFocus={Platform.isTV && isActive}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -158,7 +162,7 @@ const RatingSourceToggle = memo(({ ratingSource, setRatingSource, theme }: {
|
||||||
>
|
>
|
||||||
{source.toUpperCase()}
|
{source.toUpperCase()}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
FlatList,
|
FlatList,
|
||||||
SectionList,
|
SectionList,
|
||||||
|
|
@ -58,6 +57,7 @@ import StreamCard from '../components/StreamCard';
|
||||||
import AnimatedImage from '../components/AnimatedImage';
|
import AnimatedImage from '../components/AnimatedImage';
|
||||||
import AnimatedText from '../components/AnimatedText';
|
import AnimatedText from '../components/AnimatedText';
|
||||||
import AnimatedView from '../components/AnimatedView';
|
import AnimatedView from '../components/AnimatedView';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Lazy-safe community blur import for Android
|
// Lazy-safe community blur import for Android
|
||||||
let AndroidBlurView: any = null;
|
let AndroidBlurView: any = null;
|
||||||
|
|
@ -1850,19 +1850,23 @@ export const StreamsScreen = () => {
|
||||||
<View
|
<View
|
||||||
style={[styles.backButtonContainer, isTablet && styles.backButtonContainerTablet]}
|
style={[styles.backButtonContainer, isTablet && styles.backButtonContainerTablet]}
|
||||||
>
|
>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.backButton,
|
styles.backButton,
|
||||||
Platform.OS === 'android' ? { paddingTop: 45 } : null
|
Platform.OS === 'android' ? { paddingTop: 45 } : null
|
||||||
]}
|
]}
|
||||||
onPress={handleBack}
|
onPress={handleBack}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="listRow"
|
||||||
|
focusBorderRadius={999}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
||||||
<Text style={styles.backButtonText}>
|
<Text style={styles.backButtonText}>
|
||||||
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? 'Back to Episodes' : 'Back to Info'}
|
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode ? 'Back to Episodes' : 'Back to Info'}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -2105,12 +2109,16 @@ export const StreamsScreen = () => {
|
||||||
<Text style={styles.noStreamsSubText}>
|
<Text style={styles.noStreamsSubText}>
|
||||||
Please add streaming sources in settings
|
Please add streaming sources in settings
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.addSourcesButton}
|
style={styles.addSourcesButton}
|
||||||
onPress={() => navigation.navigate('Addons')}
|
onPress={() => navigation.navigate('Addons')}
|
||||||
|
enableTVFocus={Platform.isTV}
|
||||||
|
preset="button"
|
||||||
|
focusBorderRadius={16}
|
||||||
|
hasTVPreferredFocus={Platform.isTV}
|
||||||
>
|
>
|
||||||
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
|
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : streamsEmpty ? (
|
) : streamsEmpty ? (
|
||||||
showInitialLoading ? (
|
showInitialLoading ? (
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
|
@ -29,6 +28,7 @@ import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
// (duplicate import removed)
|
// (duplicate import removed)
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
||||||
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
|
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
|
||||||
|
|
@ -516,13 +516,13 @@ const TMDBSettingsScreen = () => {
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
<View style={[styles.headerContainer, { paddingTop: topSpacing }]}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
|
<MaterialIcons name="chevron-left" size={28} color={currentTheme.colors.primary} />
|
||||||
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
|
||||||
TMDb Settings
|
TMDb Settings
|
||||||
|
|
@ -592,12 +592,12 @@ const TMDBSettingsScreen = () => {
|
||||||
Current: {(settings.tmdbLanguagePreference || 'en').toUpperCase()}
|
Current: {(settings.tmdbLanguagePreference || 'en').toUpperCase()}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => setLanguagePickerVisible(true)}
|
onPress={() => setLanguagePickerVisible(true)}
|
||||||
style={[styles.languageButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.languageButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
|
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Logo Preview */}
|
{/* Logo Preview */}
|
||||||
|
|
@ -617,7 +617,7 @@ const TMDBSettingsScreen = () => {
|
||||||
style={styles.showsScrollView}
|
style={styles.showsScrollView}
|
||||||
>
|
>
|
||||||
{EXAMPLE_SHOWS.map((show) => (
|
{EXAMPLE_SHOWS.map((show) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={show.imdbId}
|
key={show.imdbId}
|
||||||
style={[
|
style={[
|
||||||
styles.showItem,
|
styles.showItem,
|
||||||
|
|
@ -636,7 +636,7 @@ const TMDBSettingsScreen = () => {
|
||||||
>
|
>
|
||||||
{show.name}
|
{show.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
|
@ -725,29 +725,29 @@ const TMDBSettingsScreen = () => {
|
||||||
onFocus={() => setIsInputFocused(true)}
|
onFocus={() => setIsInputFocused(true)}
|
||||||
onBlur={() => setIsInputFocused(false)}
|
onBlur={() => setIsInputFocused(false)}
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.pasteButton}
|
style={styles.pasteButton}
|
||||||
onPress={pasteFromClipboard}
|
onPress={pasteFromClipboard}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
|
<MaterialIcons name="content-paste" size={20} color={currentTheme.colors.primary} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.buttonRow}>
|
<View style={styles.buttonRow}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
onPress={saveApiKey}
|
onPress={saveApiKey}
|
||||||
>
|
>
|
||||||
<Text style={[styles.buttonText, { color: currentTheme.colors.white }]}>Save</Text>
|
<Text style={[styles.buttonText, { color: currentTheme.colors.white }]}>Save</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{isKeySet && (
|
{isKeySet && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.button, styles.clearButton, { borderColor: currentTheme.colors.error }]}
|
style={[styles.button, styles.clearButton, { borderColor: currentTheme.colors.error }]}
|
||||||
onPress={clearApiKey}
|
onPress={clearApiKey}
|
||||||
>
|
>
|
||||||
<Text style={[styles.buttonText, { color: currentTheme.colors.error }]}>Clear</Text>
|
<Text style={[styles.buttonText, { color: currentTheme.colors.error }]}>Clear</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -771,7 +771,7 @@ const TMDBSettingsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.helpLink}
|
style={styles.helpLink}
|
||||||
onPress={openTMDBWebsite}
|
onPress={openTMDBWebsite}
|
||||||
>
|
>
|
||||||
|
|
@ -779,7 +779,7 @@ const TMDBSettingsScreen = () => {
|
||||||
<Text style={[styles.helpText, { color: currentTheme.colors.primary }]}>
|
<Text style={[styles.helpText, { color: currentTheme.colors.primary }]}>
|
||||||
How to get a TMDb API key?
|
How to get a TMDb API key?
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -805,7 +805,7 @@ const TMDBSettingsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.button, { backgroundColor: currentTheme.colors.error }]}
|
style={[styles.button, { backgroundColor: currentTheme.colors.error }]}
|
||||||
onPress={handleClearCache}
|
onPress={handleClearCache}
|
||||||
>
|
>
|
||||||
|
|
@ -813,7 +813,7 @@ const TMDBSettingsScreen = () => {
|
||||||
<MaterialIcons name="delete-outline" size={18} color={currentTheme.colors.white} />
|
<MaterialIcons name="delete-outline" size={18} color={currentTheme.colors.white} />
|
||||||
<Text style={[styles.buttonText, { color: currentTheme.colors.white, marginLeft: 8 }]}>Clear Cache</Text>
|
<Text style={[styles.buttonText, { color: currentTheme.colors.white, marginLeft: 8 }]}>Clear Cache</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={[styles.infoContainer, { marginTop: 12 }]}>
|
<View style={[styles.infoContainer, { marginTop: 12 }]}>
|
||||||
<MaterialIcons name="info-outline" size={18} color={currentTheme.colors.primary} />
|
<MaterialIcons name="info-outline" size={18} color={currentTheme.colors.primary} />
|
||||||
|
|
@ -856,9 +856,9 @@ const TMDBSettingsScreen = () => {
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
/>
|
/>
|
||||||
{languageSearch.length > 0 && (
|
{languageSearch.length > 0 && (
|
||||||
<TouchableOpacity onPress={() => setLanguageSearch('')} style={styles.searchClearButton}>
|
<FocusableTouchableOpacity onPress={() => setLanguageSearch('')} style={styles.searchClearButton}>
|
||||||
<MaterialIcons name="close" size={20} color={currentTheme.colors.mediumEmphasis} />
|
<MaterialIcons name="close" size={20} color={currentTheme.colors.mediumEmphasis} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -880,7 +880,7 @@ const TMDBSettingsScreen = () => {
|
||||||
{ code: 'de', label: 'DE' },
|
{ code: 'de', label: 'DE' },
|
||||||
{ code: 'tr', label: 'TR' },
|
{ code: 'tr', label: 'TR' },
|
||||||
].map(({ code, label }) => (
|
].map(({ code, label }) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={code}
|
key={code}
|
||||||
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -899,7 +899,7 @@ const TMDBSettingsScreen = () => {
|
||||||
]}>
|
]}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -956,7 +956,7 @@ const TMDBSettingsScreen = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{filteredLanguages.map(({ code, label, native }) => (
|
{filteredLanguages.map(({ code, label, native }) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
key={code}
|
key={code}
|
||||||
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
|
||||||
style={[
|
style={[
|
||||||
|
|
@ -992,7 +992,7 @@ const TMDBSettingsScreen = () => {
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
))}
|
))}
|
||||||
{languageSearch.length > 0 && filteredLanguages.length === 0 && (
|
{languageSearch.length > 0 && filteredLanguages.length === 0 && (
|
||||||
<View style={styles.noResultsContainer}>
|
<View style={styles.noResultsContainer}>
|
||||||
|
|
@ -1000,12 +1000,12 @@ const TMDBSettingsScreen = () => {
|
||||||
<Text style={[styles.noResultsText, { color: currentTheme.colors.mediumEmphasis }]}>
|
<Text style={[styles.noResultsText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||||
No languages found for "{languageSearch}"
|
No languages found for "{languageSearch}"
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => setLanguageSearch('')}
|
onPress={() => setLanguageSearch('')}
|
||||||
style={[styles.clearSearchButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
style={[styles.clearSearchButton, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.clearSearchButtonText, { color: currentTheme.colors.primary }]}>Clear search</Text>
|
<Text style={[styles.clearSearchButtonText, { color: currentTheme.colors.primary }]}>Clear search</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
@ -1016,18 +1016,18 @@ const TMDBSettingsScreen = () => {
|
||||||
|
|
||||||
{/* Footer Actions */}
|
{/* Footer Actions */}
|
||||||
<View style={styles.modalFooter}>
|
<View style={styles.modalFooter}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => setLanguagePickerVisible(false)}
|
onPress={() => setLanguagePickerVisible(false)}
|
||||||
style={styles.cancelButton}
|
style={styles.cancelButton}
|
||||||
>
|
>
|
||||||
<Text style={[styles.cancelButtonText, { color: currentTheme.colors.text }]}>Cancel</Text>
|
<Text style={[styles.cancelButtonText, { color: currentTheme.colors.text }]}>Cancel</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => setLanguagePickerVisible(false)}
|
onPress={() => setLanguagePickerVisible(false)}
|
||||||
style={[styles.doneButton, { backgroundColor: currentTheme.colors.primary }]}
|
style={[styles.doneButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
>
|
>
|
||||||
<Text style={[styles.doneButtonText, { color: currentTheme.colors.white }]}>Done</Text>
|
<Text style={[styles.doneButtonText, { color: currentTheme.colors.white }]}>Done</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
Switch,
|
Switch,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Platform,
|
Platform,
|
||||||
|
|
@ -24,6 +23,7 @@ import { useTheme, Theme, DEFAULT_THEMES } from '../contexts/ThemeContext';
|
||||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
import { useSettings } from '../hooks/useSettings';
|
import { useSettings } from '../hooks/useSettings';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ const ThemeCard: React.FC<ThemeCardProps> = ({
|
||||||
onDelete
|
onDelete
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.themeCard,
|
styles.themeCard,
|
||||||
isSelected && styles.selectedThemeCard,
|
isSelected && styles.selectedThemeCard,
|
||||||
|
|
@ -85,24 +85,24 @@ const ThemeCard: React.FC<ThemeCardProps> = ({
|
||||||
{theme.isEditable && (
|
{theme.isEditable && (
|
||||||
<View style={styles.themeCardActions}>
|
<View style={styles.themeCardActions}>
|
||||||
{onEdit && (
|
{onEdit && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.themeCardAction, styles.buttonShadow]}
|
style={[styles.themeCardAction, styles.buttonShadow]}
|
||||||
onPress={onEdit}
|
onPress={onEdit}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="edit" size={16} color={theme.colors.primary} />
|
<MaterialIcons name="edit" size={16} color={theme.colors.primary} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{onDelete && (
|
{onDelete && (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[styles.themeCardAction, styles.buttonShadow]}
|
style={[styles.themeCardAction, styles.buttonShadow]}
|
||||||
onPress={onDelete}
|
onPress={onDelete}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="delete" size={16} color={theme.colors.error} />
|
<MaterialIcons name="delete" size={16} color={theme.colors.error} />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ const FilterTab: React.FC<FilterTabProps> = ({
|
||||||
onPress,
|
onPress,
|
||||||
primaryColor
|
primaryColor
|
||||||
}) => (
|
}) => (
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.filterTab,
|
styles.filterTab,
|
||||||
isActive && { backgroundColor: primaryColor },
|
isActive && { backgroundColor: primaryColor },
|
||||||
|
|
@ -136,7 +136,7 @@ const FilterTab: React.FC<FilterTabProps> = ({
|
||||||
>
|
>
|
||||||
{category.name}
|
{category.name}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
);
|
);
|
||||||
|
|
||||||
type ColorKey = 'primary' | 'secondary' | 'darkBackground';
|
type ColorKey = 'primary' | 'secondary' | 'darkBackground';
|
||||||
|
|
@ -242,12 +242,12 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
||||||
return (
|
return (
|
||||||
<View style={styles.editorContainer}>
|
<View style={styles.editorContainer}>
|
||||||
<View style={styles.editorHeader}>
|
<View style={styles.editorHeader}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.editorBackButton}
|
style={styles.editorBackButton}
|
||||||
onPress={onCancel}
|
onPress={onCancel}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="arrow-back" size={20} color="#FFFFFF" />
|
<MaterialIcons name="arrow-back" size={20} color="#FFFFFF" />
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.editorTitleInput}
|
style={styles.editorTitleInput}
|
||||||
value={themeName}
|
value={themeName}
|
||||||
|
|
@ -255,12 +255,12 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
||||||
placeholder="Theme name"
|
placeholder="Theme name"
|
||||||
placeholderTextColor="rgba(255,255,255,0.5)"
|
placeholderTextColor="rgba(255,255,255,0.5)"
|
||||||
/>
|
/>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.editorSaveButton}
|
style={styles.editorSaveButton}
|
||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
>
|
>
|
||||||
<Text style={styles.saveButtonText}>Save</Text>
|
<Text style={styles.saveButtonText}>Save</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.editorBody}>
|
<View style={styles.editorBody}>
|
||||||
|
|
@ -268,7 +268,7 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
||||||
<ThemePreview />
|
<ThemePreview />
|
||||||
|
|
||||||
<View style={styles.colorButtonsColumn}>
|
<View style={styles.colorButtonsColumn}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.colorSelectorButton,
|
styles.colorSelectorButton,
|
||||||
selectedColorKey === 'primary' && styles.selectedColorButton,
|
selectedColorKey === 'primary' && styles.selectedColorButton,
|
||||||
|
|
@ -277,9 +277,9 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
||||||
onPress={() => setSelectedColorKey('primary')}
|
onPress={() => setSelectedColorKey('primary')}
|
||||||
>
|
>
|
||||||
<Text style={styles.colorButtonText}>Primary</Text>
|
<Text style={styles.colorButtonText}>Primary</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.colorSelectorButton,
|
styles.colorSelectorButton,
|
||||||
selectedColorKey === 'secondary' && styles.selectedColorButton,
|
selectedColorKey === 'secondary' && styles.selectedColorButton,
|
||||||
|
|
@ -288,9 +288,9 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
||||||
onPress={() => setSelectedColorKey('secondary')}
|
onPress={() => setSelectedColorKey('secondary')}
|
||||||
>
|
>
|
||||||
<Text style={styles.colorButtonText}>Secondary</Text>
|
<Text style={styles.colorButtonText}>Secondary</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.colorSelectorButton,
|
styles.colorSelectorButton,
|
||||||
selectedColorKey === 'darkBackground' && styles.selectedColorButton,
|
selectedColorKey === 'darkBackground' && styles.selectedColorButton,
|
||||||
|
|
@ -299,7 +299,7 @@ const ThemeColorEditor: React.FC<ThemeColorEditorProps & {
|
||||||
onPress={() => setSelectedColorKey('darkBackground')}
|
onPress={() => setSelectedColorKey('darkBackground')}
|
||||||
>
|
>
|
||||||
<Text style={styles.colorButtonText}>Background</Text>
|
<Text style={styles.colorButtonText}>Background</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -535,7 +535,7 @@ const ThemeScreen: React.FC = () => {
|
||||||
<StatusBar barStyle="light-content" />
|
<StatusBar barStyle="light-content" />
|
||||||
|
|
||||||
<View style={[styles.header, { paddingTop: headerTopPadding }]}>
|
<View style={[styles.header, { paddingTop: headerTopPadding }]}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
>
|
>
|
||||||
|
|
@ -543,7 +543,7 @@ const ThemeScreen: React.FC = () => {
|
||||||
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
|
||||||
Settings
|
Settings
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
{/* Empty for now, but ready for future actions */}
|
{/* Empty for now, but ready for future actions */}
|
||||||
|
|
@ -595,7 +595,7 @@ const ThemeScreen: React.FC = () => {
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.createButton,
|
styles.createButton,
|
||||||
{ backgroundColor: currentTheme.colors.primary },
|
{ backgroundColor: currentTheme.colors.primary },
|
||||||
|
|
@ -605,7 +605,7 @@ const ThemeScreen: React.FC = () => {
|
||||||
>
|
>
|
||||||
<MaterialIcons name="add" size={20} color="#FFFFFF" />
|
<MaterialIcons name="add" size={20} color="#FFFFFF" />
|
||||||
<Text style={styles.createButtonText}>Create Custom Theme</Text>
|
<Text style={styles.createButtonText}>Create Custom Theme</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.textMuted, marginTop: 24 }]}>
|
<Text style={[styles.sectionTitle, { color: currentTheme.colors.textMuted, marginTop: 24 }]}>
|
||||||
OPTIONS
|
OPTIONS
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
|
@ -24,6 +23,7 @@ import { useTraktIntegration } from '../hooks/useTraktIntegration';
|
||||||
import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings';
|
import { useTraktAutosyncSettings } from '../hooks/useTraktAutosyncSettings';
|
||||||
import { colors } from '../styles';
|
import { colors } from '../styles';
|
||||||
import CustomAlert from '../components/CustomAlert';
|
import CustomAlert from '../components/CustomAlert';
|
||||||
|
import { FocusableTouchableOpacity } from '../components/common/FocusableTouchableOpacity';
|
||||||
|
|
||||||
// Check if running on TV platform
|
// Check if running on TV platform
|
||||||
const isTV = Platform.isTV;
|
const isTV = Platform.isTV;
|
||||||
|
|
@ -246,7 +246,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
]}>
|
]}>
|
||||||
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
onPress={() => navigation.goBack()}
|
onPress={() => navigation.goBack()}
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
>
|
>
|
||||||
|
|
@ -258,7 +258,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
<Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
|
<Text style={[styles.backText, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
|
||||||
Settings
|
Settings
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.headerActions}>
|
<View style={styles.headerActions}>
|
||||||
{/* Empty for now, but ready for future actions */}
|
{/* Empty for now, but ready for future actions */}
|
||||||
|
|
@ -328,7 +328,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.button,
|
styles.button,
|
||||||
styles.signOutButton,
|
styles.signOutButton,
|
||||||
|
|
@ -337,7 +337,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
onPress={handleSignOut}
|
onPress={handleSignOut}
|
||||||
>
|
>
|
||||||
<Text style={styles.buttonText}>Sign Out</Text>
|
<Text style={styles.buttonText}>Sign Out</Text>
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.signInContainer}>
|
<View style={styles.signInContainer}>
|
||||||
|
|
@ -358,7 +358,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
]}>
|
]}>
|
||||||
Sync your watch history, watchlist, and collection with Trakt.tv
|
Sync your watch history, watchlist, and collection with Trakt.tv
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.button,
|
styles.button,
|
||||||
{ backgroundColor: isDarkMode ? currentTheme.colors.primary : currentTheme.colors.primary }
|
{ backgroundColor: isDarkMode ? currentTheme.colors.primary : currentTheme.colors.primary }
|
||||||
|
|
@ -373,7 +373,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
Sign In with Trakt
|
Sign In with Trakt
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -448,7 +448,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<FocusableTouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.button,
|
styles.button,
|
||||||
{
|
{
|
||||||
|
|
@ -478,7 +478,7 @@ const TraktSettingsScreen: React.FC = () => {
|
||||||
Sync Now
|
Sync Now
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</FocusableTouchableOpacity>
|
||||||
|
|
||||||
{/* Display Settings Section */}
|
{/* Display Settings Section */}
|
||||||
<Text style={[
|
<Text style={[
|
||||||
|
|
|
||||||
21
src/styles/tvFocus.ts
Normal file
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