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 {
View,
Text,
@ -83,7 +83,7 @@ interface ContinueWatchingRef {
refresh: () => Promise<boolean>;
}
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 (
<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)' },
index === menuOptions.length - 1 && styles.lastMenuOption
]}
onPress={() => {
onOptionSelect(option.action);
onClose();
}}
onPress={() => handleOptionSelect(option.action)}
>
<MaterialIcons
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
@ -225,9 +233,9 @@ const DropUpMenu = ({ visible, onClose, item, onOptionSelect }: DropUpMenuProps)
</GestureHandlerRootView>
</Modal>
);
};
});
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) => {
</View>
</TouchableOpacity>
<DropUpMenu
visible={menuVisible}
onClose={handleMenuClose}
item={localItem}
onOptionSelect={handleOptionSelect}
/>
{menuVisible && (
<DropUpMenu
visible={menuVisible}
onClose={handleMenuClose}
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)
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 (
<View style={styles.catalogContainer}>
@ -356,7 +377,7 @@ const SkeletonCatalog = () => {
</View>
</View>
);
};
});
const HomeScreen = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -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 (
<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 (
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar
@ -551,81 +577,84 @@ const HomeScreen = () => {
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>
<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}
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>
);
}
}, [
isLoading,
isRefreshing,
currentTheme.colors,
showHeroSection,
featuredContent,
isSaved,
handleSaveToLibrary,
hasContinueWatching,
catalogs,
catalogsLoading,
handleRefresh,
navigation,
featuredContentSource
]);
return (
<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>
);
return isLoading && !isRefreshing ? renderLoadingScreen : renderMainContent;
};
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();
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="<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) {
logger.error(`[HDRezka] Failed to get translator ID: ${error}`);
return null;