alert orientation fix

This commit is contained in:
tapframe 2025-12-19 23:38:33 +05:30
parent 2d5b1263b5
commit 5804959ddf
11 changed files with 242 additions and 214 deletions

View file

@ -101,6 +101,7 @@ const AnnouncementOverlay: React.FC<AnnouncementOverlayProps> = ({
transparent transparent
animationType="none" animationType="none"
statusBarTranslucent statusBarTranslucent
supportedOrientations={['portrait', 'landscape']}
onRequestClose={handleClose} onRequestClose={handleClose}
> >
<View style={styles.overlay}> <View style={styles.overlay}>

View file

@ -98,7 +98,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
const isWatched = !!isWatchedProp; const isWatched = !!isWatchedProp;
const inTraktWatchlist = isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show'); const inTraktWatchlist = isAuthenticated && isInWatchlist(item.id, item.type as 'movie' | 'show');
const inTraktCollection = isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show'); const inTraktCollection = isAuthenticated && isInCollection(item.id, item.type as 'movie' | 'show');
let menuOptions = [ let menuOptions = [
{ {
icon: 'bookmark', icon: 'bookmark',
@ -152,6 +152,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
visible={visible} visible={visible}
transparent transparent
animationType="none" animationType="none"
supportedOrientations={['portrait', 'landscape']}
onRequestClose={onClose} onRequestClose={onClose}
> >
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
@ -162,7 +163,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
<View style={styles.dragHandle} /> <View style={styles.dragHandle} />
<View style={styles.menuHeader}> <View style={styles.menuHeader}>
<FastImage <FastImage
source={{ source={{
uri: item.poster, uri: item.poster,
priority: FastImage.priority.high, priority: FastImage.priority.high,
cache: FastImage.cacheControl.immutable cache: FastImage.cacheControl.immutable

View file

@ -1660,6 +1660,7 @@ const SeriesContentComponent: React.FC<SeriesContentProps> = ({
animationType="fade" animationType="fade"
onRequestClose={closeEpisodeActionMenu} onRequestClose={closeEpisodeActionMenu}
statusBarTranslucent statusBarTranslucent
supportedOrientations={['portrait', 'landscape']}
> >
<Pressable <Pressable
style={{ style={{

View file

@ -74,7 +74,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
// Enhanced responsive sizing for tablets and TV screens // Enhanced responsive sizing for tablets and TV screens
const deviceWidth = Dimensions.get('window').width; const deviceWidth = Dimensions.get('window').width;
const deviceHeight = Dimensions.get('window').height; const deviceHeight = Dimensions.get('window').height;
// Determine device type based on width // Determine device type based on width
const getDeviceType = useCallback(() => { const getDeviceType = useCallback(() => {
if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; if (deviceWidth >= BREAKPOINTS.tv) return 'tv';
@ -82,13 +82,13 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet';
return 'phone'; return 'phone';
}, [deviceWidth]); }, [deviceWidth]);
const deviceType = getDeviceType(); const deviceType = getDeviceType();
const isTablet = deviceType === 'tablet'; const isTablet = deviceType === 'tablet';
const isLargeTablet = deviceType === 'largeTablet'; const isLargeTablet = deviceType === 'largeTablet';
const isTV = deviceType === 'tv'; const isTV = deviceType === 'tv';
const isLargeScreen = isTablet || isLargeTablet || isTV; const isLargeScreen = isTablet || isLargeTablet || isTV;
// Enhanced spacing and padding // Enhanced spacing and padding
const horizontalPadding = useMemo(() => { const horizontalPadding = useMemo(() => {
switch (deviceType) { switch (deviceType) {
@ -102,7 +102,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
return 16; // phone return 16; // phone
} }
}, [deviceType]); }, [deviceType]);
// Enhanced trailer card sizing // Enhanced trailer card sizing
const trailerCardWidth = useMemo(() => { const trailerCardWidth = useMemo(() => {
switch (deviceType) { switch (deviceType) {
@ -116,7 +116,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
return 200; // phone return 200; // phone
} }
}, [deviceType]); }, [deviceType]);
const trailerCardSpacing = useMemo(() => { const trailerCardSpacing = useMemo(() => {
switch (deviceType) { switch (deviceType) {
case 'tv': case 'tv':
@ -293,7 +293,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
// Auto-select the first available category, preferring "Trailer" // Auto-select the first available category, preferring "Trailer"
const availableCategories = Object.keys(categorized); const availableCategories = Object.keys(categorized);
const preferredCategory = availableCategories.includes('Trailer') ? 'Trailer' : const preferredCategory = availableCategories.includes('Trailer') ? 'Trailer' :
availableCategories.includes('Teaser') ? 'Teaser' : availableCategories[0]; availableCategories.includes('Teaser') ? 'Teaser' : availableCategories[0];
setSelectedCategory(preferredCategory); setSelectedCategory(preferredCategory);
} }
} catch (err) { } catch (err) {
@ -379,7 +379,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
} catch (error) { } catch (error) {
logger.warn('TrailersSection', 'Error pausing hero trailer:', error); logger.warn('TrailersSection', 'Error pausing hero trailer:', error);
} }
setSelectedTrailer(trailer); setSelectedTrailer(trailer);
setModalVisible(true); setModalVisible(true);
}; };
@ -499,15 +499,15 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
return ( return (
<Animated.View style={[ <Animated.View style={[
styles.container, styles.container,
sectionAnimatedStyle, sectionAnimatedStyle,
{ paddingHorizontal: horizontalPadding } { paddingHorizontal: horizontalPadding }
]}> ]}>
{/* Enhanced Header with Category Selector */} {/* Enhanced Header with Category Selector */}
<View style={styles.header}> <View style={styles.header}>
<Text style={[ <Text style={[
styles.headerTitle, styles.headerTitle,
{ {
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20 fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20
} }
@ -519,8 +519,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
{trailerCategories.length > 0 && selectedCategory && ( {trailerCategories.length > 0 && selectedCategory && (
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.categorySelector, styles.categorySelector,
{ {
borderColor: 'rgba(255,255,255,0.6)', borderColor: 'rgba(255,255,255,0.6)',
paddingHorizontal: isTV ? 14 : isLargeTablet ? 12 : isTablet ? 10 : 10, paddingHorizontal: isTV ? 14 : isLargeTablet ? 12 : isTablet ? 10 : 10,
paddingVertical: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 5 : 5, paddingVertical: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 5 : 5,
@ -533,8 +533,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
> >
<Text <Text
style={[ style={[
styles.categorySelectorText, styles.categorySelectorText,
{ {
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12, fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12,
maxWidth: isTV ? 150 : isLargeTablet ? 130 : isTablet ? 120 : 120 maxWidth: isTV ? 150 : isLargeTablet ? 130 : isTablet ? 120 : 120
@ -559,6 +559,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
visible={dropdownVisible} visible={dropdownVisible}
transparent={true} transparent={true}
animationType="fade" animationType="fade"
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => setDropdownVisible(false)} onRequestClose={() => setDropdownVisible(false)}
> >
<TouchableOpacity <TouchableOpacity
@ -587,7 +588,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
> >
<View style={styles.dropdownItemContent}> <View style={styles.dropdownItemContent}>
<View style={[ <View style={[
styles.categoryIconContainer, styles.categoryIconContainer,
{ {
backgroundColor: currentTheme.colors.primary + '15', backgroundColor: currentTheme.colors.primary + '15',
width: isTV ? 36 : isLargeTablet ? 32 : isTablet ? 28 : 28, width: isTV ? 36 : isLargeTablet ? 32 : isTablet ? 28 : 28,
@ -601,18 +602,18 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
color={currentTheme.colors.primary} color={currentTheme.colors.primary}
/> />
</View> </View>
<Text style={[ <Text style={[
styles.dropdownItemText, styles.dropdownItemText,
{ {
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 16 fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 16
} }
]}> ]}>
{formatTrailerType(category)} {formatTrailerType(category)}
</Text> </Text>
<Text style={[ <Text style={[
styles.dropdownItemCount, styles.dropdownItemCount,
{ {
color: currentTheme.colors.textMuted, color: currentTheme.colors.textMuted,
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12, fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12,
paddingHorizontal: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8, paddingHorizontal: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8,
@ -690,8 +691,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
<View style={styles.trailerInfoBelow}> <View style={styles.trailerInfoBelow}>
<Text <Text
style={[ style={[
styles.trailerTitle, styles.trailerTitle,
{ {
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12, fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12,
lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 16, lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 16,
@ -704,8 +705,8 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
{trailer.displayName || trailer.name} {trailer.displayName || trailer.name}
</Text> </Text>
<Text style={[ <Text style={[
styles.trailerMeta, styles.trailerMeta,
{ {
color: currentTheme.colors.textMuted, color: currentTheme.colors.textMuted,
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 10 fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 10
} }

View file

@ -135,10 +135,10 @@ const AndroidVideoPlayer: React.FC = () => {
// Helper to get dynamic volume icon // Helper to get dynamic volume icon
const getVolumeIcon = (value: number) => { const getVolumeIcon = (value: number) => {
if (value === 0) return 'volume-off'; if (value === 0) return 'volume-off';
if (value < 0.3) return 'volume-mute'; if (value < 0.3) return 'volume-mute';
if (value < 0.6) return 'volume-down'; if (value < 0.6) return 'volume-down';
return 'volume-up'; return 'volume-up';
}; };
// Helper to get dynamic brightness icon // Helper to get dynamic brightness icon
@ -3432,8 +3432,6 @@ const AndroidVideoPlayer: React.FC = () => {
buffered={buffered} buffered={buffered}
formatTime={formatTime} formatTime={formatTime}
playerBackend={useVLC ? 'VLC' : 'ExoPlayer'} playerBackend={useVLC ? 'VLC' : 'ExoPlayer'}
nextLoadingTitle={nextLoadingTitle}
controlsFixedOffset={Math.min(Dimensions.get('window').width, Dimensions.get('window').height) >= 768 ? 120 : 100}
/> />
{/* Combined Volume & Brightness Gesture Indicator - NEW PILL STYLE (No Bar) */} {/* Combined Volume & Brightness Gesture Indicator - NEW PILL STYLE (No Bar) */}
@ -3441,45 +3439,45 @@ const AndroidVideoPlayer: React.FC = () => {
<View style={localStyles.gestureIndicatorContainer}> <View style={localStyles.gestureIndicatorContainer}>
{/* Dynamic Icon */} {/* Dynamic Icon */}
<View <View
style={[ style={[
localStyles.iconWrapper, localStyles.iconWrapper,
{ {
// Conditional Background Color Logic // Conditional Background Color Logic
backgroundColor: gestureControls.showVolumeOverlay && volume === 0 backgroundColor: gestureControls.showVolumeOverlay && volume === 0
? 'rgba(242, 184, 181)' ? 'rgba(242, 184, 181)'
: 'rgba(59, 59, 59)' : 'rgba(59, 59, 59)'
} }
]} ]}
> >
<MaterialIcons <MaterialIcons
name={ name={
gestureControls.showVolumeOverlay gestureControls.showVolumeOverlay
? getVolumeIcon(volume) ? getVolumeIcon(volume)
: getBrightnessIcon(brightness) : getBrightnessIcon(brightness)
} }
size={24} // Reduced size to fit inside a 32-40px circle better size={24} // Reduced size to fit inside a 32-40px circle better
color={ color={
gestureControls.showVolumeOverlay && volume === 0 gestureControls.showVolumeOverlay && volume === 0
? 'rgba(96, 20, 16)' // Bright RED for MUTE icon itself ? 'rgba(96, 20, 16)' // Bright RED for MUTE icon itself
: 'rgba(255, 255, 255)' // White for all other states : 'rgba(255, 255, 255)' // White for all other states
} }
/> />
</View> </View>
{/* Text Label: Shows "Muted" or percentage */} {/* Text Label: Shows "Muted" or percentage */}
<Text <Text
style={[ style={[
localStyles.gestureText, localStyles.gestureText,
// Conditional Text Color Logic // Conditional Text Color Logic
gestureControls.showVolumeOverlay && volume === 0 && { color: 'rgba(242, 184, 181)' } // Light RED for "Muted" gestureControls.showVolumeOverlay && volume === 0 && { color: 'rgba(242, 184, 181)' } // Light RED for "Muted"
]} ]}
> >
{/* Conditional Text Content Logic */} {/* Conditional Text Content Logic */}
{gestureControls.showVolumeOverlay && volume === 0 {gestureControls.showVolumeOverlay && volume === 0
? "Muted" // Display "Muted" when volume is 0 ? "Muted" // Display "Muted" when volume is 0
: `${Math.round((gestureControls.showVolumeOverlay ? volume : brightness) * 100)}%` // Display percentage otherwise : `${Math.round((gestureControls.showVolumeOverlay ? volume : brightness) * 100)}%` // Display percentage otherwise
} }
</Text> </Text>
</View> </View>
)} )}
@ -4067,32 +4065,32 @@ const AndroidVideoPlayer: React.FC = () => {
// New styles for the gesture indicator // New styles for the gesture indicator
const localStyles = StyleSheet.create({ const localStyles = StyleSheet.create({
gestureIndicatorContainer: { gestureIndicatorContainer: {
position: 'absolute', position: 'absolute',
top: '4%', // Adjust this for vertical position top: '4%', // Adjust this for vertical position
alignSelf: 'center', // Adjust this for horizontal position alignSelf: 'center', // Adjust this for horizontal position
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
backgroundColor: 'rgba(25, 25, 25)', // Dark pill background backgroundColor: 'rgba(25, 25, 25)', // Dark pill background
borderRadius: 70, borderRadius: 70,
paddingHorizontal: 15, paddingHorizontal: 15,
paddingVertical: 15, paddingVertical: 15,
zIndex: 2000, // Very high z-index to ensure visibility zIndex: 2000, // Very high z-index to ensure visibility
minWidth: 120, // Adjusted min width since bar is removed minWidth: 120, // Adjusted min width since bar is removed
}, },
iconWrapper: { iconWrapper: {
borderRadius: 50, // Makes it a perfect circle (set to a high number) borderRadius: 50, // Makes it a perfect circle (set to a high number)
width: 40, // Define the diameter of the circle width: 40, // Define the diameter of the circle
height: 40, // Define the diameter of the circle height: 40, // Define the diameter of the circle
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 12, // Margin to separate icon circle from percentage text marginRight: 12, // Margin to separate icon circle from percentage text
}, },
gestureText: { gestureText: {
color: '#FFFFFF', color: '#FFFFFF',
fontSize: 18, fontSize: 18,
fontWeight: 'normal', fontWeight: 'normal',
minWidth: 35, minWidth: 35,
textAlign: 'right', textAlign: 'right',
}, },
}); });

View file

@ -1413,6 +1413,7 @@ const AddonsScreen = () => {
visible={showConfirmModal} visible={showConfirmModal}
transparent transparent
animationType="fade" animationType="fade"
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => { onRequestClose={() => {
setShowConfirmModal(false); setShowConfirmModal(false);
setAddonDetails(null); setAddonDetails(null);

View file

@ -685,6 +685,7 @@ const CatalogSettingsScreen = () => {
animationType="fade" animationType="fade"
transparent={true} transparent={true}
visible={isRenameModalVisible} visible={isRenameModalVisible}
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => { onRequestClose={() => {
setIsRenameModalVisible(false); setIsRenameModalVisible(false);
setCatalogToRename(null); setCatalogToRename(null);

View file

@ -1946,6 +1946,7 @@ const PluginsScreen: React.FC = () => {
visible={showHelpModal} visible={showHelpModal}
transparent={true} transparent={true}
animationType="fade" animationType="fade"
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => setShowHelpModal(false)} onRequestClose={() => setShowHelpModal(false)}
> >
<View style={styles.modalOverlay}> <View style={styles.modalOverlay}>
@ -1978,6 +1979,7 @@ const PluginsScreen: React.FC = () => {
visible={showAddRepositoryModal} visible={showAddRepositoryModal}
transparent={true} transparent={true}
animationType="fade" animationType="fade"
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => setShowAddRepositoryModal(false)} onRequestClose={() => setShowAddRepositoryModal(false)}
> >
<View style={styles.modalOverlay}> <View style={styles.modalOverlay}>

View file

@ -33,7 +33,7 @@ const ProfilesScreen: React.FC = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext(); const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
const [profiles, setProfiles] = useState<Profile[]>([]); const [profiles, setProfiles] = useState<Profile[]>([]);
const [showAddModal, setShowAddModal] = useState(false); const [showAddModal, setShowAddModal] = useState(false);
const [newProfileName, setNewProfileName] = useState(''); const [newProfileName, setNewProfileName] = useState('');
@ -52,7 +52,7 @@ const ProfilesScreen: React.FC = () => {
) => { ) => {
setAlertTitle(title); setAlertTitle(title);
setAlertMessage(message); setAlertMessage(message);
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => {} }]); setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
setAlertVisible(true); setAlertVisible(true);
}; };
@ -92,7 +92,7 @@ const ProfilesScreen: React.FC = () => {
} }
}); });
}); });
return unsubscribe; return unsubscribe;
}, [navigation, refreshAuthStatus, isAuthenticated, loadProfiles]); }, [navigation, refreshAuthStatus, isAuthenticated, loadProfiles]);
@ -112,7 +112,7 @@ const ProfilesScreen: React.FC = () => {
navigation.goBack(); navigation.goBack();
return; return;
} }
loadProfiles(); loadProfiles();
}, [isAuthenticated, loadProfiles, navigation]); }, [isAuthenticated, loadProfiles, navigation]);
@ -141,7 +141,7 @@ const ProfilesScreen: React.FC = () => {
...profile, ...profile,
isActive: profile.id === id isActive: profile.id === id
})); }));
setProfiles(updatedProfiles); setProfiles(updatedProfiles);
saveProfiles(updatedProfiles); saveProfiles(updatedProfiles);
}, [profiles, saveProfiles]); }, [profiles, saveProfiles]);
@ -164,14 +164,14 @@ const ProfilesScreen: React.FC = () => {
'Delete Profile', 'Delete Profile',
'Are you sure you want to delete this profile? This action cannot be undone.', 'Are you sure you want to delete this profile? This action cannot be undone.',
[ [
{ label: 'Cancel', onPress: () => {} }, { label: 'Cancel', onPress: () => { } },
{ {
label: 'Delete', label: 'Delete',
onPress: () => { onPress: () => {
const updatedProfiles = profiles.filter(profile => profile.id !== id); const updatedProfiles = profiles.filter(profile => profile.id !== id);
setProfiles(updatedProfiles); setProfiles(updatedProfiles);
saveProfiles(updatedProfiles); saveProfiles(updatedProfiles);
} }
} }
] ]
); );
@ -183,10 +183,10 @@ const ProfilesScreen: React.FC = () => {
const renderItem = ({ item }: { item: Profile }) => ( const renderItem = ({ item }: { item: Profile }) => (
<View style={styles.profileItem}> <View style={styles.profileItem}>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.profileContent, styles.profileContent,
item.isActive && { item.isActive && {
backgroundColor: `${currentTheme.colors.primary}30`, backgroundColor: `${currentTheme.colors.primary}30`,
borderColor: currentTheme.colors.primary borderColor: currentTheme.colors.primary
} }
@ -194,10 +194,10 @@ const ProfilesScreen: React.FC = () => {
onPress={() => handleSelectProfile(item.id)} onPress={() => handleSelectProfile(item.id)}
> >
<View style={styles.avatarContainer}> <View style={styles.avatarContainer}>
<MaterialIcons <MaterialIcons
name="account-circle" name="account-circle"
size={40} size={40}
color={item.isActive ? currentTheme.colors.primary : currentTheme.colors.text} color={item.isActive ? currentTheme.colors.primary : currentTheme.colors.text}
/> />
</View> </View>
<View style={styles.profileInfo}> <View style={styles.profileInfo}>
@ -211,7 +211,7 @@ const ProfilesScreen: React.FC = () => {
)} )}
</View> </View>
{!item.isActive && ( {!item.isActive && (
<TouchableOpacity <TouchableOpacity
style={styles.deleteButton} style={styles.deleteButton}
onPress={() => handleDeleteProfile(item.id)} onPress={() => handleDeleteProfile(item.id)}
> >
@ -225,7 +225,7 @@ const ProfilesScreen: React.FC = () => {
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent /> <StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity <TouchableOpacity
onPress={handleBack} onPress={handleBack}
@ -281,6 +281,7 @@ const ProfilesScreen: React.FC = () => {
visible={showAddModal} visible={showAddModal}
transparent transparent
animationType="fade" animationType="fade"
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => setShowAddModal(false)} onRequestClose={() => setShowAddModal(false)}
> >
<View style={styles.modalOverlay}> <View style={styles.modalOverlay}>
@ -288,11 +289,11 @@ const ProfilesScreen: React.FC = () => {
<Text style={[styles.modalTitle, { color: currentTheme.colors.text }]}> <Text style={[styles.modalTitle, { color: currentTheme.colors.text }]}>
Create New Profile Create New Profile
</Text> </Text>
<TextInput <TextInput
style={[ style={[
styles.input, styles.input,
{ {
backgroundColor: `${currentTheme.colors.textMuted}20`, backgroundColor: `${currentTheme.colors.textMuted}20`,
color: currentTheme.colors.text, color: currentTheme.colors.text,
borderColor: currentTheme.colors.border borderColor: currentTheme.colors.border
@ -304,9 +305,9 @@ const ProfilesScreen: React.FC = () => {
onChangeText={setNewProfileName} onChangeText={setNewProfileName}
autoFocus autoFocus
/> />
<View style={styles.modalButtons}> <View style={styles.modalButtons}>
<TouchableOpacity <TouchableOpacity
style={[styles.modalButton, styles.cancelButton]} style={[styles.modalButton, styles.cancelButton]}
onPress={() => { onPress={() => {
setNewProfileName(''); setNewProfileName('');
@ -315,9 +316,9 @@ const ProfilesScreen: React.FC = () => {
> >
<Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text> <Text style={{ color: currentTheme.colors.textMuted }}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[ style={[
styles.modalButton, styles.modalButton,
styles.createButton, styles.createButton,
{ backgroundColor: currentTheme.colors.primary } { backgroundColor: currentTheme.colors.primary }
]} ]}

View file

@ -132,17 +132,27 @@ export const StreamsScreen = () => {
const { showSuccess, showInfo } = useToast(); const { showSuccess, showInfo } = useToast();
// Add dimension listener and tablet detection // Add dimension listener and tablet detection
// Use a ref to track previous dimensions to avoid unnecessary re-renders
const [dimensions, setDimensions] = useState(Dimensions.get('window')); const [dimensions, setDimensions] = useState(Dimensions.get('window'));
const prevDimensionsRef = useRef({ width: dimensions.width, height: dimensions.height });
useEffect(() => { useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ window }) => { const subscription = Dimensions.addEventListener('change', ({ window }) => {
setDimensions(window); // Only update state if dimensions actually changed (with 1px tolerance)
const widthChanged = Math.abs(window.width - prevDimensionsRef.current.width) > 1;
const heightChanged = Math.abs(window.height - prevDimensionsRef.current.height) > 1;
if (widthChanged || heightChanged) {
prevDimensionsRef.current = { width: window.width, height: window.height };
setDimensions(window);
}
}); });
return () => subscription?.remove(); return () => subscription?.remove();
}, []); }, []);
// Memoize tablet detection to prevent recalculation on every render
const deviceWidth = dimensions.width; const deviceWidth = dimensions.width;
const isTablet = deviceWidth >= 768; const isTablet = useMemo(() => deviceWidth >= 768, [deviceWidth]);
// Add refs to prevent excessive updates and duplicate loads // Add refs to prevent excessive updates and duplicate loads
const isMounted = useRef(true); const isMounted = useRef(true);
@ -303,6 +313,9 @@ export const StreamsScreen = () => {
}, []); }, []);
// Monitor streams loading and update available providers immediately // Monitor streams loading and update available providers immediately
// Use a ref to track the previous providers to avoid unnecessary state updates
const prevProvidersRef = useRef<Set<string>>(new Set());
useEffect(() => { useEffect(() => {
// Skip processing if component is unmounting // Skip processing if component is unmounting
if (!isMounted.current) return; if (!isMounted.current) return;
@ -317,14 +330,21 @@ export const StreamsScreen = () => {
if (providersWithStreams.length > 0) { if (providersWithStreams.length > 0) {
logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`); logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`);
const providersWithStreamsSet = new Set(providersWithStreams);
// Only update if we have new providers, don't remove existing ones during loading // Check if we actually have new providers before triggering state update
setAvailableProviders(prevProviders => { const hasNewProviders = providersWithStreams.some(
const newProviders = new Set([...prevProviders, ...providersWithStreamsSet]); provider => !prevProvidersRef.current.has(provider)
if (__DEV__) console.log('[StreamsScreen] availableProviders ->', Array.from(newProviders)); );
return newProviders;
}); if (hasNewProviders) {
setAvailableProviders(prevProviders => {
const newProviders = new Set([...prevProviders, ...providersWithStreams]);
// Update ref to track current providers
prevProvidersRef.current = newProviders;
if (__DEV__) console.log('[StreamsScreen] availableProviders ->', Array.from(newProviders));
return newProviders;
});
}
} }
// Update loading states for individual providers // Update loading states for individual providers

View file

@ -36,27 +36,27 @@ const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
// Define example shows with their IMDB IDs and TMDB IDs // Define example shows with their IMDB IDs and TMDB IDs
const EXAMPLE_SHOWS = [ const EXAMPLE_SHOWS = [
{ {
name: 'Breaking Bad', name: 'Breaking Bad',
imdbId: 'tt0903747', imdbId: 'tt0903747',
tmdbId: '1396', tmdbId: '1396',
type: 'tv' as const type: 'tv' as const
}, },
{ {
name: 'Friends', name: 'Friends',
imdbId: 'tt0108778', imdbId: 'tt0108778',
tmdbId: '1668', tmdbId: '1668',
type: 'tv' as const type: 'tv' as const
}, },
{ {
name: 'Stranger Things', name: 'Stranger Things',
imdbId: 'tt4574334', imdbId: 'tt4574334',
tmdbId: '66732', tmdbId: '66732',
type: 'tv' as const type: 'tv' as const
}, },
{ {
name: 'Avatar', name: 'Avatar',
imdbId: 'tt0499549', imdbId: 'tt0499549',
tmdbId: '19995', tmdbId: '19995',
type: 'movie' as const type: 'movie' as const
}, },
@ -82,7 +82,7 @@ const TMDBSettingsScreen = () => {
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
const [languagePickerVisible, setLanguagePickerVisible] = useState(false); const [languagePickerVisible, setLanguagePickerVisible] = useState(false);
const [languageSearch, setLanguageSearch] = useState(''); const [languageSearch, setLanguageSearch] = useState('');
// Logo preview state // Logo preview state
const [selectedShow, setSelectedShow] = useState(EXAMPLE_SHOWS[0]); const [selectedShow, setSelectedShow] = useState(EXAMPLE_SHOWS[0]);
const [tmdbLogo, setTmdbLogo] = useState<string | null>(null); const [tmdbLogo, setTmdbLogo] = useState<string | null>(null);
@ -126,7 +126,7 @@ const TMDBSettingsScreen = () => {
try { try {
const keys = await mmkvStorage.getAllKeys(); const keys = await mmkvStorage.getAllKeys();
const tmdbKeys = keys.filter(key => key.startsWith('tmdb_cache_')); const tmdbKeys = keys.filter(key => key.startsWith('tmdb_cache_'));
let totalSize = 0; let totalSize = 0;
for (const key of tmdbKeys) { for (const key of tmdbKeys) {
const value = mmkvStorage.getString(key); const value = mmkvStorage.getString(key);
@ -134,7 +134,7 @@ const TMDBSettingsScreen = () => {
totalSize += value.length; totalSize += value.length;
} }
} }
// Convert to KB/MB // Convert to KB/MB
let sizeStr = ''; let sizeStr = '';
if (totalSize < 1024) { if (totalSize < 1024) {
@ -144,7 +144,7 @@ const TMDBSettingsScreen = () => {
} else { } else {
sizeStr = `${(totalSize / (1024 * 1024)).toFixed(2)} MB`; sizeStr = `${(totalSize / (1024 * 1024)).toFixed(2)} MB`;
} }
setCacheSize(sizeStr); setCacheSize(sizeStr);
} catch (error) { } catch (error) {
logger.error('[TMDBSettingsScreen] Error calculating cache size:', error); logger.error('[TMDBSettingsScreen] Error calculating cache size:', error);
@ -187,17 +187,17 @@ const TMDBSettingsScreen = () => {
mmkvStorage.getItem(TMDB_API_KEY_STORAGE_KEY), mmkvStorage.getItem(TMDB_API_KEY_STORAGE_KEY),
mmkvStorage.getItem(USE_CUSTOM_TMDB_API_KEY) mmkvStorage.getItem(USE_CUSTOM_TMDB_API_KEY)
]); ]);
logger.log('[TMDBSettingsScreen] API key status:', savedKey ? 'Found' : 'Not found'); logger.log('[TMDBSettingsScreen] API key status:', savedKey ? 'Found' : 'Not found');
logger.log('[TMDBSettingsScreen] Use custom API setting:', savedUseCustomKey); logger.log('[TMDBSettingsScreen] Use custom API setting:', savedUseCustomKey);
if (savedKey) { if (savedKey) {
setApiKey(savedKey); setApiKey(savedKey);
setIsKeySet(true); setIsKeySet(true);
} else { } else {
setIsKeySet(false); setIsKeySet(false);
} }
setUseCustomKey(savedUseCustomKey === 'true'); setUseCustomKey(savedUseCustomKey === 'true');
} catch (error) { } catch (error) {
logger.error('[TMDBSettingsScreen] Failed to load settings:', error); logger.error('[TMDBSettingsScreen] Failed to load settings:', error);
@ -212,7 +212,7 @@ const TMDBSettingsScreen = () => {
const saveApiKey = async () => { const saveApiKey = async () => {
logger.log('[TMDBSettingsScreen] Starting API key save'); logger.log('[TMDBSettingsScreen] Starting API key save');
Keyboard.dismiss(); Keyboard.dismiss();
try { try {
const trimmedKey = apiKey.trim(); const trimmedKey = apiKey.trim();
if (!trimmedKey) { if (!trimmedKey) {
@ -299,27 +299,27 @@ const TMDBSettingsScreen = () => {
try { try {
await mmkvStorage.setItem(USE_CUSTOM_TMDB_API_KEY, value ? 'true' : 'false'); await mmkvStorage.setItem(USE_CUSTOM_TMDB_API_KEY, value ? 'true' : 'false');
setUseCustomKey(value); setUseCustomKey(value);
if (!value) { if (!value) {
// If switching to built-in key, show confirmation // If switching to built-in key, show confirmation
logger.log('[TMDBSettingsScreen] Switching to built-in API key'); logger.log('[TMDBSettingsScreen] Switching to built-in API key');
setTestResult({ setTestResult({
success: true, success: true,
message: 'Now using the built-in TMDb API key.' message: 'Now using the built-in TMDb API key.'
}); });
} else if (apiKey && isKeySet) { } else if (apiKey && isKeySet) {
// If switching to custom key and we have a key // If switching to custom key and we have a key
logger.log('[TMDBSettingsScreen] Switching to custom API key'); logger.log('[TMDBSettingsScreen] Switching to custom API key');
setTestResult({ setTestResult({
success: true, success: true,
message: 'Now using your custom TMDb API key.' message: 'Now using your custom TMDb API key.'
}); });
} else { } else {
// If switching to custom key but don't have a key yet // If switching to custom key but don't have a key yet
logger.log('[TMDBSettingsScreen] No custom key available yet'); logger.log('[TMDBSettingsScreen] No custom key available yet');
setTestResult({ setTestResult({
success: false, success: false,
message: 'Please enter and save your custom TMDb API key.' message: 'Please enter and save your custom TMDb API key.'
}); });
} }
} catch (error) { } catch (error) {
@ -355,27 +355,27 @@ const TMDBSettingsScreen = () => {
setLoadingLogos(true); setLoadingLogos(true);
setTmdbLogo(null); setTmdbLogo(null);
setTmdbBanner(null); setTmdbBanner(null);
try { try {
const tmdbId = show.tmdbId; const tmdbId = show.tmdbId;
const contentType = show.type; const contentType = show.type;
logger.log(`[TMDBSettingsScreen] Fetching ${show.name} with TMDB ID: ${tmdbId}`); logger.log(`[TMDBSettingsScreen] Fetching ${show.name} with TMDB ID: ${tmdbId}`);
const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en'; const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en';
const apiKey = TMDB_API_KEY; const apiKey = TMDB_API_KEY;
const endpoint = contentType === 'tv' ? 'tv' : 'movie'; const endpoint = contentType === 'tv' ? 'tv' : 'movie';
const response = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}/images?api_key=${apiKey}`); const response = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}/images?api_key=${apiKey}`);
const imagesData = await response.json(); const imagesData = await response.json();
if (imagesData.logos && imagesData.logos.length > 0) { if (imagesData.logos && imagesData.logos.length > 0) {
let logoPath: string | null = null; let logoPath: string | null = null;
let logoLanguage = preferredTmdbLanguage; let logoLanguage = preferredTmdbLanguage;
// Try to find logo in preferred language // Try to find logo in preferred language
const preferredLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === preferredTmdbLanguage); const preferredLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === preferredTmdbLanguage);
if (preferredLogo) { if (preferredLogo) {
logoPath = preferredLogo.file_path; logoPath = preferredLogo.file_path;
logoLanguage = preferredTmdbLanguage; logoLanguage = preferredTmdbLanguage;
@ -383,7 +383,7 @@ const TMDBSettingsScreen = () => {
} else { } else {
// Fallback to English // Fallback to English
const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en'); const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en');
if (englishLogo) { if (englishLogo) {
logoPath = englishLogo.file_path; logoPath = englishLogo.file_path;
logoLanguage = 'en'; logoLanguage = 'en';
@ -395,7 +395,7 @@ const TMDBSettingsScreen = () => {
setIsPreviewFallback(true); setIsPreviewFallback(true);
} }
} }
if (logoPath) { if (logoPath) {
setTmdbLogo(`https://image.tmdb.org/t/p/original${logoPath}`); setTmdbLogo(`https://image.tmdb.org/t/p/original${logoPath}`);
setPreviewLanguage(logoLanguage); setPreviewLanguage(logoLanguage);
@ -407,7 +407,7 @@ const TMDBSettingsScreen = () => {
setPreviewLanguage(''); setPreviewLanguage('');
setIsPreviewFallback(false); setIsPreviewFallback(false);
} }
// Get TMDB banner (backdrop) // Get TMDB banner (backdrop)
if (imagesData.backdrops && imagesData.backdrops.length > 0) { if (imagesData.backdrops && imagesData.backdrops.length > 0) {
const backdropPath = imagesData.backdrops[0].file_path; const backdropPath = imagesData.backdrops[0].file_path;
@ -415,7 +415,7 @@ const TMDBSettingsScreen = () => {
} else { } else {
const detailsResponse = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}?api_key=${apiKey}`); const detailsResponse = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}?api_key=${apiKey}`);
const details = await detailsResponse.json(); const details = await detailsResponse.json();
if (details.backdrop_path) { if (details.backdrop_path) {
setTmdbBanner(`https://image.tmdb.org/t/p/original${details.backdrop_path}`); setTmdbBanner(`https://image.tmdb.org/t/p/original${details.backdrop_path}`);
} }
@ -444,17 +444,17 @@ const TMDBSettingsScreen = () => {
</View> </View>
); );
} }
return ( return (
<View style={styles.bannerContainer}> <View style={styles.bannerContainer}>
<FastImage <FastImage
source={{ uri: banner || undefined }} source={{ uri: banner || undefined }}
style={styles.bannerImage} style={styles.bannerImage}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
<View style={styles.bannerOverlay} /> <View style={styles.bannerOverlay} />
{logo && ( {logo && (
<FastImage <FastImage
source={{ uri: logo }} source={{ uri: logo }}
style={styles.logoOverBanner} style={styles.logoOverBanner}
resizeMode={FastImage.resizeMode.contain} resizeMode={FastImage.resizeMode.contain}
@ -491,7 +491,7 @@ const TMDBSettingsScreen = () => {
if (__DEV__) console.error('Error loading selected show:', e); if (__DEV__) console.error('Error loading selected show:', e);
} }
}; };
loadSelectedShow(); loadSelectedShow();
}, []); }, []);
@ -512,7 +512,7 @@ const TMDBSettingsScreen = () => {
} }
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<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}>
@ -520,7 +520,7 @@ const TMDBSettingsScreen = () => {
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> </TouchableOpacity>
</View> </View>
@ -602,7 +602,7 @@ const TMDBSettingsScreen = () => {
{/* Logo Preview */} {/* Logo Preview */}
<View style={styles.divider} /> <View style={styles.divider} />
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 8 }]}>Logo Preview</Text> <Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 8 }]}>Logo Preview</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, marginBottom: 12 }]}> <Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, marginBottom: 12 }]}>
Preview shows how localized logos will appear in the selected language. Preview shows how localized logos will appear in the selected language.
@ -610,8 +610,8 @@ const TMDBSettingsScreen = () => {
{/* Show selector */} {/* Show selector */}
<Text style={[styles.selectorLabel, { color: currentTheme.colors.mediumEmphasis }]}>Example:</Text> <Text style={[styles.selectorLabel, { color: currentTheme.colors.mediumEmphasis }]}>Example:</Text>
<ScrollView <ScrollView
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.showsScrollContent} contentContainerStyle={styles.showsScrollContent}
style={styles.showsScrollView} style={styles.showsScrollView}
@ -627,7 +627,7 @@ const TMDBSettingsScreen = () => {
onPress={() => handleShowSelect(show)} onPress={() => handleShowSelect(show)}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text <Text
style={[ style={[
styles.showItemText, styles.showItemText,
{ color: currentTheme.colors.mediumEmphasis }, { color: currentTheme.colors.mediumEmphasis },
@ -795,7 +795,7 @@ const TMDBSettingsScreen = () => {
{/* Cache Management Section */} {/* Cache Management Section */}
<View style={styles.divider} /> <View style={styles.divider} />
<View style={styles.settingRow}> <View style={styles.settingRow}>
<View style={styles.settingTextContainer}> <View style={styles.settingTextContainer}>
<Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Cache Size</Text> <Text style={[styles.settingTitle, { color: currentTheme.colors.text }]}>Cache Size</Text>
@ -828,6 +828,7 @@ const TMDBSettingsScreen = () => {
visible={languagePickerVisible} visible={languagePickerVisible}
transparent transparent
animationType="slide" animationType="slide"
supportedOrientations={['portrait', 'landscape']}
onRequestClose={() => setLanguagePickerVisible(false)} onRequestClose={() => setLanguagePickerVisible(false)}
> >
<TouchableWithoutFeedback onPress={() => setLanguagePickerVisible(false)}> <TouchableWithoutFeedback onPress={() => setLanguagePickerVisible(false)}>
@ -955,42 +956,42 @@ const TMDBSettingsScreen = () => {
return ( return (
<> <>
{filteredLanguages.map(({ code, label, native }) => ( {filteredLanguages.map(({ code, label, native }) => (
<TouchableOpacity <TouchableOpacity
key={code} key={code}
onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }} onPress={() => { updateSetting('tmdbLanguagePreference', code); setLanguagePickerVisible(false); }}
style={[ style={[
styles.languageItem, styles.languageItem,
settings.tmdbLanguagePreference === code && styles.selectedLanguageItem settings.tmdbLanguagePreference === code && styles.selectedLanguageItem
]} ]}
activeOpacity={0.7} activeOpacity={0.7}
> >
<View style={styles.languageContent}> <View style={styles.languageContent}>
<View style={styles.languageInfo}> <View style={styles.languageInfo}>
<Text style={[ <Text style={[
styles.languageName, styles.languageName,
settings.tmdbLanguagePreference === code && styles.selectedLanguageName, settings.tmdbLanguagePreference === code && styles.selectedLanguageName,
{ {
color: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.text, color: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.text,
} }
]}> ]}>
{native} {native}
</Text> </Text>
<Text style={[ <Text style={[
styles.languageCode, styles.languageCode,
settings.tmdbLanguagePreference === code && styles.selectedLanguageCode, settings.tmdbLanguagePreference === code && styles.selectedLanguageCode,
{ {
color: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.mediumEmphasis, color: settings.tmdbLanguagePreference === code ? currentTheme.colors.primary : currentTheme.colors.mediumEmphasis,
} }
]}> ]}>
{label} {code.toUpperCase()} {label} {code.toUpperCase()}
</Text> </Text>
</View> </View>
{settings.tmdbLanguagePreference === code && ( {settings.tmdbLanguagePreference === code && (
<View style={styles.checkmarkContainer}> <View style={styles.checkmarkContainer}>
<MaterialIcons name="check-circle" size={24} color={currentTheme.colors.primary} /> <MaterialIcons name="check-circle" size={24} color={currentTheme.colors.primary} />
</View> </View>
)} )}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
))} ))}
{languageSearch.length > 0 && filteredLanguages.length === 0 && ( {languageSearch.length > 0 && filteredLanguages.length === 0 && (