mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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:
parent
a4b09e6afe
commit
259d071e95
2 changed files with 185 additions and 134 deletions
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue