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;