From 259d071e95a87c1765eec31eef9b8a3864585c51 Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 27 May 2025 22:11:16 +0530 Subject: [PATCH] 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. --- src/screens/HomeScreen.tsx | 283 ++++++++++++++++++--------------- src/services/hdrezkaService.ts | 36 ++++- 2 files changed, 185 insertions(+), 134 deletions(-) diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index bd6684d..5af6b37 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { View, Text, @@ -83,7 +83,7 @@ interface ContinueWatchingRef { refresh: () => Promise; } -const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => { +const DropUpMenu = React.memo(({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => { const translateY = useSharedValue(300); const opacity = useSharedValue(0); const isDarkMode = useColorScheme() === 'dark'; @@ -98,9 +98,15 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) opacity.value = withTiming(0, { duration: 200 }); translateY.value = withTiming(300, { duration: 300 }); } + + // Cleanup animations when component unmounts + return () => { + opacity.value = 0; + translateY.value = 300; + }; }, [visible]); - const gesture = Gesture.Pan() + const gesture = useMemo(() => Gesture.Pan() .onStart(() => { // Store initial position if needed }) @@ -124,7 +130,7 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) translateY.value = withTiming(0, { duration: 300 }); opacity.value = withTiming(1, { duration: 200 }); } - }); + }), [onClose]); const overlayStyle = useAnimatedStyle(() => ({ opacity: opacity.value, @@ -138,7 +144,7 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white, })); - const menuOptions = [ + const menuOptions = useMemo(() => [ { icon: item.inLibrary ? 'bookmark' : 'bookmark-border', label: item.inLibrary ? 'Remove from Library' : 'Add to Library', @@ -159,7 +165,12 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) label: 'Share', action: 'share' } - ]; + ], [item.inLibrary]); + + const handleOptionSelect = useCallback((action: string) => { + onOptionSelect(action); + onClose(); + }, [onOptionSelect, onClose]); return ( { - onOptionSelect(option.action); - onClose(); - }} + onPress={() => handleOptionSelect(option.action)} > ); -}; +}); -const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => { +const ContentItem = React.memo(({ item: initialItem, onPress }: ContentItemProps) => { const [menuVisible, setMenuVisible] = useState(false); const [localItem, setLocalItem] = useState(initialItem); const [isWatched, setIsWatched] = useState(false); @@ -256,8 +264,8 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => { setIsWatched(prev => !prev); break; case 'playlist': - break; case 'share': + // These options don't have implementations yet break; } }, [localItem]); @@ -266,16 +274,20 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => { setMenuVisible(false); }, []); + // Only update localItem when initialItem changes useEffect(() => { setLocalItem(initialItem); }, [initialItem]); + // Subscribe to library updates useEffect(() => { const unsubscribe = catalogService.subscribeToLibraryUpdates((libraryItems) => { const isInLibrary = libraryItems.some( 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(); @@ -330,15 +342,24 @@ const ContentItem = ({ item: initialItem, onPress }: ContentItemProps) => { - + {menuVisible && ( + + )} ); -}; +}, (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) const SAMPLE_CATEGORIES: Category[] = [ @@ -347,7 +368,7 @@ const SAMPLE_CATEGORIES: Category[] = [ { id: 'channel', name: 'Channels' }, ]; -const SkeletonCatalog = () => { +const SkeletonCatalog = React.memo(() => { const { currentTheme } = useTheme(); return ( @@ -356,7 +377,7 @@ const SkeletonCatalog = () => { ); -}; +}); const HomeScreen = () => { const navigation = useNavigation>(); @@ -385,7 +406,11 @@ const HomeScreen = () => { } = useFeaturedContent(); // 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; // React to settings changes @@ -399,9 +424,6 @@ const HomeScreen = () => { const handleSettingsChange = () => { setShowHeroSection(settings.showHeroSection); 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 @@ -410,18 +432,6 @@ const HomeScreen = () => { return unsubscribe; }, [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( useCallback(() => { const statusBarConfig = () => { @@ -451,16 +461,15 @@ const HomeScreen = () => { StatusBar.setTranslucent(false); StatusBar.setBackgroundColor(currentTheme.colors.darkBackground); } + + // Clean up any lingering timeouts + if (refreshTimeoutRef.current) { + clearTimeout(refreshTimeoutRef.current); + } }; }, [currentTheme.colors.darkBackground]); - useEffect(() => { - navigation.addListener('beforeRemove', () => {}); - return () => { - navigation.removeListener('beforeRemove', () => {}); - }; - }, [navigation]); - + // Preload images function - memoized to avoid recreating on every render const preloadImages = useCallback(async (content: StreamingContent[]) => { if (!content.length) return; @@ -530,20 +539,37 @@ const HomeScreen = () => { }, []); useEffect(() => { - const handlePlaybackComplete = () => { - refreshContinueWatching(); - }; - const unsubscribe = navigation.addListener('focus', () => { refreshContinueWatching(); }); - return () => { - unsubscribe(); - }; + return unsubscribe; }, [navigation, refreshContinueWatching]); - if (isLoading && !isRefreshing) { + // Memoize the loading screen to prevent unnecessary re-renders + const renderLoadingScreen = useMemo(() => { + if (isLoading && !isRefreshing) { + return ( + + + + + Loading your content... + + + ); + } + return null; + }, [isLoading, isRefreshing, currentTheme.colors]); + + // Memoize the main content section + const renderMainContent = useMemo(() => { + if (isLoading && !isRefreshing) return null; + return ( { backgroundColor="transparent" translucent /> - - - Loading your content... - + + } + contentContainerStyle={[ + styles.scrollContent, + { paddingTop: Platform.OS === 'ios' ? 100 : 90 } + ]} + showsVerticalScrollIndicator={false} + removeClippedSubviews={true} + > + {showHeroSection && ( + + )} + + + + + + {hasContinueWatching && ( + + + + )} + + {catalogs.length > 0 ? ( + catalogs.map((catalog, index) => ( + + + + )) + ) : ( + !catalogsLoading && ( + + + + No content available + + navigation.navigate('Settings')} + > + + Add Catalogs + + + ) + )} + ); - } + }, [ + isLoading, + isRefreshing, + currentTheme.colors, + showHeroSection, + featuredContent, + isSaved, + handleSaveToLibrary, + hasContinueWatching, + catalogs, + catalogsLoading, + handleRefresh, + navigation, + featuredContentSource + ]); - return ( - - - - } - contentContainerStyle={[ - styles.scrollContent, - { paddingTop: Platform.OS === 'ios' ? 100 : 90 } - ]} - showsVerticalScrollIndicator={false} - > - {showHeroSection && ( - - )} - - - - - - {hasContinueWatching && ( - - - - )} - - {catalogs.length > 0 ? ( - catalogs.map((catalog, index) => ( - - - - )) - ) : ( - !catalogsLoading && ( - - - - No content available - - navigation.navigate('Settings')} - > - - Add Catalogs - - - ) - )} - - - ); + return isLoading && !isRefreshing ? renderLoadingScreen : renderMainContent; }; const { width, height } = Dimensions.get('window'); @@ -1045,4 +1074,4 @@ const styles = StyleSheet.create({ }, }); -export default HomeScreen; \ No newline at end of file +export default React.memo(HomeScreen); \ No newline at end of file diff --git a/src/services/hdrezkaService.ts b/src/services/hdrezkaService.ts index 9d97ca7..eb3dd6c 100644 --- a/src/services/hdrezkaService.ts +++ b/src/services/hdrezkaService.ts @@ -280,19 +280,41 @@ class HDRezkaService { const responseText = await response.text(); 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"`)) { - logger.log(`[HDRezka] Found translator ID 238 (Original + subtitles)`); + logger.log(`[HDRezka] Found specific translator ID 238 (Original + subtitles)`); return '238'; } + // 2. Try to extract from the main CDN init function (e.g., initCDNMoviesEvents, initCDNSeriesEvents) const functionName = mediaType === 'movie' ? 'initCDNMoviesEvents' : 'initCDNSeriesEvents'; - const regexPattern = new RegExp(`sof\\.tv\\.${functionName}\\(${id}, ([^,]+)`, 'i'); - const match = responseText.match(regexPattern); - const translatorId = match ? match[1] : null; + const cdnEventsRegex = new RegExp(`sof\.tv\.${functionName}\(${id}, ([^,]+)`, 'i'); + const cdnEventsMatch = responseText.match(cdnEventsRegex); - logger.log(`[HDRezka] Extracted translator ID: ${translatorId}`); - return translatorId; + if (cdnEventsMatch && cdnEventsMatch[1]) { + 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="" + 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) { logger.error(`[HDRezka] Failed to get translator ID: ${error}`); return null;