Refactor HomeScreen and DropUpMenu components for performance and clarity

This update enhances the HomeScreen and DropUpMenu components by implementing React.memo for performance optimization and using useMemo and useCallback hooks to prevent unnecessary re-renders. Additionally, the loading state management has been improved, and the logic for handling menu options has been streamlined. The changes contribute to a more efficient rendering process and a cleaner codebase, enhancing the overall user experience.
This commit is contained in:
tapframe 2025-05-27 22:11:16 +05:30
parent a4b09e6afe
commit 259d071e95
2 changed files with 185 additions and 134 deletions

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { import {
View, View,
Text, Text,
@ -83,7 +83,7 @@ interface ContinueWatchingRef {
refresh: () => Promise<boolean>; refresh: () => Promise<boolean>;
} }
const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => { const DropUpMenu = React.memo(({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => {
const translateY = useSharedValue(300); const translateY = useSharedValue(300);
const opacity = useSharedValue(0); const opacity = useSharedValue(0);
const isDarkMode = useColorScheme() === 'dark'; const isDarkMode = useColorScheme() === 'dark';
@ -98,9 +98,15 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
opacity.value = withTiming(0, { duration: 200 }); opacity.value = withTiming(0, { duration: 200 });
translateY.value = withTiming(300, { duration: 300 }); translateY.value = withTiming(300, { duration: 300 });
} }
// Cleanup animations when component unmounts
return () => {
opacity.value = 0;
translateY.value = 300;
};
}, [visible]); }, [visible]);
const gesture = Gesture.Pan() const gesture = useMemo(() => Gesture.Pan()
.onStart(() => { .onStart(() => {
// Store initial position if needed // Store initial position if needed
}) })
@ -124,7 +130,7 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
translateY.value = withTiming(0, { duration: 300 }); translateY.value = withTiming(0, { duration: 300 });
opacity.value = withTiming(1, { duration: 200 }); opacity.value = withTiming(1, { duration: 200 });
} }
}); }), [onClose]);
const overlayStyle = useAnimatedStyle(() => ({ const overlayStyle = useAnimatedStyle(() => ({
opacity: opacity.value, opacity: opacity.value,
@ -138,7 +144,7 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white, backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white,
})); }));
const menuOptions = [ const menuOptions = useMemo(() => [
{ {
icon: item.inLibrary ? 'bookmark' : 'bookmark-border', icon: item.inLibrary ? 'bookmark' : 'bookmark-border',
label: item.inLibrary ? 'Remove from Library' : 'Add to Library', label: item.inLibrary ? 'Remove from Library' : 'Add to Library',
@ -159,7 +165,12 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
label: 'Share', label: 'Share',
action: 'share' action: 'share'
} }
]; ], [item.inLibrary]);
const handleOptionSelect = useCallback((action: string) => {
onOptionSelect(action);
onClose();
}, [onOptionSelect, onClose]);
return ( return (
<Modal <Modal
@ -200,10 +211,7 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)' }, { borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)' },
index === menuOptions.length - 1 && styles.lastMenuOption index === menuOptions.length - 1 && styles.lastMenuOption
]} ]}
onPress={() => { onPress={() => handleOptionSelect(option.action)}
onOptionSelect(option.action);
onClose();
}}
> >
<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"}
@ -225,9 +233,9 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
</GestureHandlerRootView> </GestureHandlerRootView>
</Modal> </Modal>
); );
}; });
const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => { const ContentItem = React.memo(({ item: initialItem, onPress }: ContentItemProps) => {
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
const [localItem, setLocalItem] = useState(initialItem); const [localItem, setLocalItem] = useState(initialItem);
const [isWatched, setIsWatched] = useState(false); const [isWatched, setIsWatched] = useState(false);
@ -256,8 +264,8 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
setIsWatched(prev => !prev); setIsWatched(prev => !prev);
break; break;
case 'playlist': case 'playlist':
break;
case 'share': case 'share':
// These options don't have implementations yet
break; break;
} }
}, [localItem]); }, [localItem]);
@ -266,16 +274,20 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
setMenuVisible(false); setMenuVisible(false);
}, []); }, []);
// Only update localItem when initialItem changes
useEffect(() => { useEffect(() => {
setLocalItem(initialItem); setLocalItem(initialItem);
}, [initialItem]); }, [initialItem]);
// Subscribe to library updates
useEffect(() => { useEffect(() => {
const unsubscribe = catalogService.subscribeToLibraryUpdates((libraryItems) => { const unsubscribe = catalogService.subscribeToLibraryUpdates((libraryItems) => {
const isInLibrary = libraryItems.some( const isInLibrary = libraryItems.some(
libraryItem => libraryItem.id === localItem.id && libraryItem.type === localItem.type libraryItem => libraryItem.id === localItem.id && libraryItem.type === localItem.type
); );
setLocalItem(prev => ({ ...prev, inLibrary: isInLibrary })); if (isInLibrary !== localItem.inLibrary) {
setLocalItem(prev => ({ ...prev, inLibrary: isInLibrary }));
}
}); });
return () => unsubscribe(); return () => unsubscribe();
@ -330,15 +342,24 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => {
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<DropUpMenu {menuVisible && (
visible={menuVisible} <DropUpMenu
onClose={handleMenuClose} visible={menuVisible}
item={localItem} onClose={handleMenuClose}
onOptionSelect={handleOptionSelect} item={localItem}
/> onOptionSelect={handleOptionSelect}
/>
)}
</> </>
); );
}; }, (prevProps, nextProps) => {
// Custom comparison function to prevent unnecessary re-renders
return (
prevProps.item.id === nextProps.item.id &&
prevProps.item.inLibrary === nextProps.item.inLibrary &&
prevProps.onPress === nextProps.onPress
);
});
// Sample categories (real app would get these from API) // Sample categories (real app would get these from API)
const SAMPLE_CATEGORIES: Category[] = [ const SAMPLE_CATEGORIES: Category[] = [
@ -347,7 +368,7 @@ const SAMPLE_CATEGORIES: Category[] = [
{ id: 'channel', name: 'Channels' }, { id: 'channel', name: 'Channels' },
]; ];
const SkeletonCatalog = () => { const SkeletonCatalog = React.memo(() => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
return ( return (
<View style={styles.catalogContainer}> <View style={styles.catalogContainer}>
@ -356,7 +377,7 @@ const SkeletonCatalog = () => {
</View> </View>
</View> </View>
); );
}; });
const HomeScreen = () => { const HomeScreen = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -385,7 +406,11 @@ const HomeScreen = () => {
} = useFeaturedContent(); } = useFeaturedContent();
// Only count feature section as loading if it's enabled in settings // Only count feature section as loading if it's enabled in settings
const isLoading = (showHeroSection ? featuredLoading : false) || catalogsLoading; const isLoading = useMemo(() =>
(showHeroSection ? featuredLoading : false) || catalogsLoading,
[showHeroSection, featuredLoading, catalogsLoading]
);
const isRefreshing = catalogsRefreshing; const isRefreshing = catalogsRefreshing;
// React to settings changes // React to settings changes
@ -399,9 +424,6 @@ const HomeScreen = () => {
const handleSettingsChange = () => { const handleSettingsChange = () => {
setShowHeroSection(settings.showHeroSection); setShowHeroSection(settings.showHeroSection);
setFeaturedContentSource(settings.featuredContentSource); setFeaturedContentSource(settings.featuredContentSource);
// The featured content refresh is now handled by the useFeaturedContent hook
// No need to call refreshFeatured() here to avoid duplicate refreshes
}; };
// Subscribe to settings changes // Subscribe to settings changes
@ -410,18 +432,6 @@ const HomeScreen = () => {
return unsubscribe; return unsubscribe;
}, [settings]); }, [settings]);
// Update the featured content refresh logic to handle persistence
useEffect(() => {
// This effect was causing duplicate refreshes - it's now handled in useFeaturedContent
// We'll keep it just to sync the local state with settings
if (showHeroSection && featuredContentSource !== settings.featuredContentSource) {
// Just update the local state
setFeaturedContentSource(settings.featuredContentSource);
}
// No timeout needed since we're not refreshing here
}, [settings.featuredContentSource, showHeroSection]);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
const statusBarConfig = () => { const statusBarConfig = () => {
@ -451,16 +461,15 @@ const HomeScreen = () => {
StatusBar.setTranslucent(false); StatusBar.setTranslucent(false);
StatusBar.setBackgroundColor(currentTheme.colors.darkBackground); StatusBar.setBackgroundColor(currentTheme.colors.darkBackground);
} }
// Clean up any lingering timeouts
if (refreshTimeoutRef.current) {
clearTimeout(refreshTimeoutRef.current);
}
}; };
}, [currentTheme.colors.darkBackground]); }, [currentTheme.colors.darkBackground]);
useEffect(() => { // Preload images function - memoized to avoid recreating on every render
navigation.addListener('beforeRemove', () => {});
return () => {
navigation.removeListener('beforeRemove', () => {});
};
}, [navigation]);
const preloadImages = useCallback(async (content: StreamingContent[]) => { const preloadImages = useCallback(async (content: StreamingContent[]) => {
if (!content.length) return; if (!content.length) return;
@ -530,20 +539,37 @@ const HomeScreen = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
const handlePlaybackComplete = () => {
refreshContinueWatching();
};
const unsubscribe = navigation.addListener('focus', () => { const unsubscribe = navigation.addListener('focus', () => {
refreshContinueWatching(); refreshContinueWatching();
}); });
return () => { return unsubscribe;
unsubscribe();
};
}, [navigation, refreshContinueWatching]); }, [navigation, refreshContinueWatching]);
if (isLoading && !isRefreshing) { // Memoize the loading screen to prevent unnecessary re-renders
const renderLoadingScreen = useMemo(() => {
if (isLoading && !isRefreshing) {
return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar
barStyle="light-content"
backgroundColor="transparent"
translucent
/>
<View style={styles.loadingMainContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading your content...</Text>
</View>
</View>
);
}
return null;
}, [isLoading, isRefreshing, currentTheme.colors]);
// Memoize the main content section
const renderMainContent = useMemo(() => {
if (isLoading && !isRefreshing) return null;
return ( return (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar <StatusBar
@ -551,81 +577,84 @@ const HomeScreen = () => {
backgroundColor="transparent" backgroundColor="transparent"
translucent translucent
/> />
<View style={styles.loadingMainContainer}> <ScrollView
<ActivityIndicator size="large" color={currentTheme.colors.primary} /> refreshControl={
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>Loading your content...</Text> <RefreshControl
</View> refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor={currentTheme.colors.primary}
colors={[currentTheme.colors.primary, currentTheme.colors.secondary]}
/>
}
contentContainerStyle={[
styles.scrollContent,
{ paddingTop: Platform.OS === 'ios' ? 100 : 90 }
]}
showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
>
{showHeroSection && (
<FeaturedContent
key={`featured-${showHeroSection}-${featuredContentSource}`}
featuredContent={featuredContent}
isSaved={isSaved}
handleSaveToLibrary={handleSaveToLibrary}
/>
)}
<Animated.View entering={FadeIn.duration(400).delay(150)}>
<ThisWeekSection />
</Animated.View>
{hasContinueWatching && (
<Animated.View entering={FadeIn.duration(400).delay(250)}>
<ContinueWatchingSection ref={continueWatchingRef} />
</Animated.View>
)}
{catalogs.length > 0 ? (
catalogs.map((catalog, index) => (
<View key={`${catalog.addon}-${catalog.id}-${index}`}>
<CatalogSection catalog={catalog} />
</View>
))
) : (
!catalogsLoading && (
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
No content available
</Text>
<TouchableOpacity
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Settings')}
>
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
</TouchableOpacity>
</View>
)
)}
</ScrollView>
</View> </View>
); );
} }, [
isLoading,
isRefreshing,
currentTheme.colors,
showHeroSection,
featuredContent,
isSaved,
handleSaveToLibrary,
hasContinueWatching,
catalogs,
catalogsLoading,
handleRefresh,
navigation,
featuredContentSource
]);
return ( return isLoading && !isRefreshing ? renderLoadingScreen : renderMainContent;
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar
barStyle="light-content"
backgroundColor="transparent"
translucent
/>
<ScrollView
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor={currentTheme.colors.primary}
colors={[currentTheme.colors.primary, currentTheme.colors.secondary]}
/>
}
contentContainerStyle={[
styles.scrollContent,
{ paddingTop: Platform.OS === 'ios' ? 100 : 90 }
]}
showsVerticalScrollIndicator={false}
>
{showHeroSection && (
<FeaturedContent
key={`featured-${showHeroSection}`}
featuredContent={featuredContent}
isSaved={isSaved}
handleSaveToLibrary={handleSaveToLibrary}
/>
)}
<Animated.View entering={FadeIn.duration(400).delay(150)}>
<ThisWeekSection />
</Animated.View>
{hasContinueWatching && (
<Animated.View entering={FadeIn.duration(400).delay(250)}>
<ContinueWatchingSection ref={continueWatchingRef} />
</Animated.View>
)}
{catalogs.length > 0 ? (
catalogs.map((catalog, index) => (
<View key={`${catalog.addon}-${catalog.id}-${index}`}>
<CatalogSection catalog={catalog} />
</View>
))
) : (
!catalogsLoading && (
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
No content available
</Text>
<TouchableOpacity
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Settings')}
>
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
</TouchableOpacity>
</View>
)
)}
</ScrollView>
</View>
);
}; };
const { width, height } = Dimensions.get('window'); const { width, height } = Dimensions.get('window');
@ -1045,4 +1074,4 @@ const styles = StyleSheet.create<any>({
}, },
}); });
export default HomeScreen; export default React.memo(HomeScreen);

View file

@ -280,19 +280,41 @@ class HDRezkaService {
const responseText = await response.text(); const responseText = await response.text();
logger.log(`[HDRezka] Translator page response length: ${responseText.length}`); logger.log(`[HDRezka] Translator page response length: ${responseText.length}`);
// Translator ID 238 represents the Original + subtitles player. // 1. Check for "Original + Subtitles" specific ID (often ID 238)
if (responseText.includes(`data-translator_id="238"`)) { if (responseText.includes(`data-translator_id="238"`)) {
logger.log(`[HDRezka] Found translator ID 238 (Original + subtitles)`); logger.log(`[HDRezka] Found specific translator ID 238 (Original + subtitles)`);
return '238'; return '238';
} }
// 2. Try to extract from the main CDN init function (e.g., initCDNMoviesEvents, initCDNSeriesEvents)
const functionName = mediaType === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents'; const functionName = mediaType === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents';
const regexPattern = new RegExp(`sof\\.tv\\.${functionName}\\(${id}, ([^,]+)`, 'i'); const cdnEventsRegex = new RegExp(`sof\.tv\.${functionName}\(${id}, ([^,]+)`, 'i');
const match = responseText.match(regexPattern); const cdnEventsMatch = responseText.match(cdnEventsRegex);
const translatorId = match ? match[1] : null;
logger.log(`[HDRezka] Extracted translator ID: ${translatorId}`); if (cdnEventsMatch && cdnEventsMatch[1]) {
return translatorId; const translatorIdFromCdn = cdnEventsMatch[1].trim().replace(/['"]/g, ''); // Remove potential quotes
if (translatorIdFromCdn && translatorIdFromCdn !== 'false' && translatorIdFromCdn !== 'null') {
logger.log(`[HDRezka] Extracted translator ID from CDN init: ${translatorIdFromCdn}`);
return translatorIdFromCdn;
}
}
logger.log(`[HDRezka] CDN init function did not yield a valid translator ID.`);
// 3. Fallback: Try to find any other data-translator_id attribute in the HTML
// This regex looks for data-translator_id="<digits>"
const anyTranslatorRegex = /data-translator_id="(\d+)"/;
const anyTranslatorMatch = responseText.match(anyTranslatorRegex);
if (anyTranslatorMatch && anyTranslatorMatch[1]) {
const fallbackTranslatorId = anyTranslatorMatch[1].trim();
logger.log(`[HDRezka] Found fallback translator ID from data attribute: ${fallbackTranslatorId}`);
return fallbackTranslatorId;
}
logger.log(`[HDRezka] No fallback data-translator_id found.`);
// If all attempts fail
logger.log(`[HDRezka] Could not find any translator ID for id ${id} on page ${fullUrl}`);
return null;
} catch (error) { } catch (error) {
logger.error(`[HDRezka] Failed to get translator ID: ${error}`); logger.error(`[HDRezka] Failed to get translator ID: ${error}`);
return null; return null;