From bc2a15f81fed9a2b52610c515c234bed2b1a969c Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 10 Sep 2025 21:38:43 +0530 Subject: [PATCH] potential heat fix --- App.tsx | 8 + android/app/src/main/AndroidManifest.xml | 2 +- src/components/UpdatePopup.tsx | 2 +- src/components/home/CatalogSection.tsx | 43 ++- src/components/home/ContentItem.tsx | 31 ++- .../home/ContinueWatchingSection.tsx | 250 +++++++++--------- src/components/home/FeaturedContent.tsx | 48 ++-- src/hooks/useFeaturedContent.ts | 34 ++- src/navigation/AppNavigator.tsx | 20 +- src/screens/HomeScreen.tsx | 182 ++++++------- src/screens/UpdateScreen.tsx | 128 ++------- src/services/updateService.ts | 43 +-- 12 files changed, 386 insertions(+), 405 deletions(-) diff --git a/App.tsx b/App.tsx index 804eca2..ea7fd54 100644 --- a/App.tsx +++ b/App.tsx @@ -59,6 +59,14 @@ enableScreens(true); // Inner app component that uses the theme context const ThemedApp = () => { + // Log JS engine once at startup + useEffect(() => { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const engine = (global as any).HermesInternal ? 'Hermes' : 'JSC'; + console.log('JS Engine:', engine); + } catch {} + }, []); const { currentTheme } = useTheme(); const [isAppReady, setIsAppReady] = useState(false); const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState(null); diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e85b4c8..4c5dee5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -16,7 +16,7 @@ - + diff --git a/src/components/UpdatePopup.tsx b/src/components/UpdatePopup.tsx index 31b2313..5033e32 100644 --- a/src/components/UpdatePopup.tsx +++ b/src/components/UpdatePopup.tsx @@ -20,7 +20,7 @@ interface UpdatePopupProps { updateInfo: { isAvailable: boolean; manifest?: { - id: string; + id?: string; version?: string; description?: string; }; diff --git a/src/components/home/CatalogSection.tsx b/src/components/home/CatalogSection.tsx index ffdbf0a..88560cf 100644 --- a/src/components/home/CatalogSection.tsx +++ b/src/components/home/CatalogSection.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native'; import { FlashList } from '@shopify/flash-list'; import { NavigationProp, useNavigation } from '@react-navigation/native'; @@ -56,23 +56,49 @@ const POSTER_WIDTH = posterLayout.posterWidth; const CatalogSection = ({ catalog }: CatalogSectionProps) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); + // Simplified visibility tracking to reduce state updates and re-renders + const [visibleIndexSet, setVisibleIndexSet] = useState>(new Set([0, 1, 2, 3, 4, 5, 6, 7])); + const viewabilityConfig = useMemo(() => ({ + itemVisiblePercentThreshold: 15, + minimumViewTime: 100, + }), []); + + const onViewableItemsChanged = useRef(({ viewableItems }: { viewableItems: Array<{ index?: number | null }> }) => { + const next = new Set(); + viewableItems.forEach(v => { if (typeof v.index === 'number') next.add(v.index); }); + // Only pre-warm immediate neighbors to reduce overhead + const neighbors: number[] = []; + next.forEach(i => { + neighbors.push(i - 1, i + 1); + }); + neighbors.forEach(i => { if (i >= 0) next.add(i); }); + setVisibleIndexSet(next); + }); + + const [minVisible, maxVisible] = useMemo(() => { + if (visibleIndexSet.size === 0) return [0, 7]; + let min = Number.POSITIVE_INFINITY; + let max = 0; + visibleIndexSet.forEach(i => { if (i < min) min = i; if (i > max) max = i; }); + return [min, max]; + }, [visibleIndexSet]); const handleContentPress = useCallback((id: string, type: string) => { navigation.navigate('Metadata', { id, type, addonId: catalog.addon }); }, [navigation, catalog.addon]); const renderContentItem = useCallback(({ item, index }: { item: StreamingContent, index: number }) => { - // Only load images for the first few items eagerly; others defer based on viewability - const eager = index < 6; + // Simplify visibility logic to reduce re-renders + const isVisible = visibleIndexSet.has(index) || index < 8; return ( ); - }, [handleContentPress]); + }, [handleContentPress, visibleIndexSet]); // Memoize the ItemSeparatorComponent to prevent re-creation const ItemSeparator = useCallback(() => , []); @@ -112,7 +138,10 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => { ItemSeparatorComponent={ItemSeparator} onEndReachedThreshold={0.7} onEndReached={() => {}} - scrollEventThrottle={32} + scrollEventThrottle={64} + viewabilityConfig={viewabilityConfig as any} + onViewableItemsChanged={onViewableItemsChanged.current as any} + removeClippedSubviews={true} /> ); diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index 8033111..3738c44 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -68,6 +68,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe const { currentTheme } = useTheme(); const { settings } = useSettings(); const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12; + // Memoize poster width calculation to avoid recalculating on every render const posterWidth = React.useMemo(() => { switch (settings.posterSize) { case 'small': @@ -130,31 +131,31 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe return () => clearTimeout(timer); }, [shouldLoadImageProp, deferMs]); - // Get optimized poster URL for smaller tiles - const getOptimizedPosterUrl = useCallback((originalUrl: string) => { - if (!originalUrl || originalUrl.includes('placeholder')) { + // Memoize optimized poster URL to prevent recalculating + const optimizedPosterUrl = React.useMemo(() => { + if (!item.poster || item.poster.includes('placeholder')) { return 'https://via.placeholder.com/154x231/333/666?text=No+Image'; } // If we've had an error, try metahub fallback - if (retryCount > 0 && !originalUrl.includes('metahub.space')) { + if (retryCount > 0 && !item.poster.includes('metahub.space')) { return `https://images.metahub.space/poster/small/${item.id}/img`; } // For TMDB images, use smaller sizes - if (originalUrl.includes('image.tmdb.org')) { + if (item.poster.includes('image.tmdb.org')) { // Replace any size with w154 (fits 100-130px tiles perfectly) - return originalUrl.replace(/\/w\d+\//, '/w154/'); + return item.poster.replace(/\/w\d+\//, '/w154/'); } // For metahub images, use smaller sizes - if (originalUrl.includes('images.metahub.space')) { - return originalUrl.replace('/medium/', '/small/'); + if (item.poster.includes('images.metahub.space')) { + return item.poster.replace('/medium/', '/small/'); } // Return original URL for other sources to avoid breaking them - return originalUrl; - }, [retryCount, item.id]); + return item.poster; + }, [item.poster, retryCount, item.id]); return ( <> @@ -170,7 +171,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe {/* Only load image when shouldLoadImage is true (lazy loading) */} {(shouldLoadImageProp ?? shouldLoadImageState) && item.poster ? ( { - // Aggressive memoization - only re-render if ID changes (different item entirely) - // This keeps loaded posters stable during fast scrolls - return prev.item.id === next.item.id; + // Re-render when identity changes or when visibility-driven loading flips + if (prev.item.id !== next.item.id) return false; + if (prev.item.poster !== next.item.poster) return false; + if ((prev.shouldLoadImage ?? false) !== (next.shouldLoadImage ?? false)) return false; + return true; }); \ No newline at end of file diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index 4ee0b0a..b63bc15 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -96,8 +96,8 @@ const ContinueWatchingSection = React.forwardRef((props, re const [deletingItemId, setDeletingItemId] = useState(null); const longPressTimeoutRef = useRef(null); - // Use a state to track if a background refresh is in progress - const [isRefreshing, setIsRefreshing] = useState(false); + // Use a ref to track if a background refresh is in progress to avoid state updates + const isRefreshingRef = useRef(false); // Cache for metadata to avoid redundant API calls const metadataCache = useRef>({}); @@ -133,12 +133,12 @@ const ContinueWatchingSection = React.forwardRef((props, re // Modified loadContinueWatching to render incrementally const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => { - if (isRefreshing) return; + if (isRefreshingRef.current) return; if (!isBackgroundRefresh) { setLoading(true); } - setIsRefreshing(true); + isRefreshingRef.current = true; // Helper to merge a batch of items into state (dedupe by type:id, keep newest) const mergeBatchIntoState = (batch: ContinueWatchingItem[]) => { @@ -361,9 +361,9 @@ const ContinueWatchingSection = React.forwardRef((props, re logger.error('Failed to load continue watching items:', error); } finally { setLoading(false); - setIsRefreshing(false); + isRefreshingRef.current = false; } - }, [isRefreshing, getCachedMetadata]); + }, [getCachedMetadata]); // Clear cache when component unmounts or when needed useEffect(() => { @@ -398,7 +398,7 @@ const ContinueWatchingSection = React.forwardRef((props, re refreshTimerRef.current = setTimeout(() => { // Trigger a background refresh loadContinueWatching(true); - }, 500); // Increased debounce time slightly + }, 2000); // Increased debounce time significantly to reduce churn }; // Try to set up a custom event listener or use a timer as fallback @@ -415,8 +415,8 @@ const ContinueWatchingSection = React.forwardRef((props, re } }; } else { - // Reduced polling frequency from 30s to 2 minutes to reduce heating - const intervalId = setInterval(() => loadContinueWatching(true), 120000); + // Reduced polling frequency from 30s to 5 minutes to reduce heating and battery drain + const intervalId = setInterval(() => loadContinueWatching(true), 300000); return () => { subscription.remove(); clearInterval(intervalId); @@ -448,7 +448,7 @@ const ContinueWatchingSection = React.forwardRef((props, re navigation.navigate('Metadata', { id, type }); }, [navigation]); - // Handle long press to delete + // Handle long press to delete (moved before renderContinueWatchingItem) const handleLongPress = useCallback((item: ContinueWatchingItem) => { try { // Trigger haptic feedback @@ -503,6 +503,119 @@ const ContinueWatchingSection = React.forwardRef((props, re ); }, []); + // Memoized render function for continue watching items + const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => ( + handleContentPress(item.id, item.type)} + onLongPress={() => handleLongPress(item)} + delayLongPress={800} + > + {/* Poster Image */} + + + + {/* Delete Indicator Overlay */} + {deletingItemId === item.id && ( + + + + )} + + + {/* Content Details */} + + + {(() => { + const isUpNext = item.progress === 0; + return ( + + + {item.name} + + {isUpNext && ( + + Up Next + + )} + + ); + })()} + + + {/* Episode Info or Year */} + {(() => { + if (item.type === 'series' && item.season && item.episode) { + return ( + + + Season {item.season} + + {item.episodeTitle && ( + + {item.episodeTitle} + + )} + + ); + } else { + return ( + + {item.year} • {item.type === 'movie' ? 'Movie' : 'Series'} + + ); + } + })()} + + {/* Progress Bar */} + {item.progress > 0 && ( + + + + + + {Math.round(item.progress)}% watched + + + )} + + + ), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId]); + + // Memoized key extractor + const keyExtractor = useCallback((item: ContinueWatchingItem) => `continue-${item.id}-${item.type}`, []); + + // Memoized item separator + const ItemSeparator = useCallback(() => , []); + // If no continue watching items, don't render anything if (continueWatchingItems.length === 0) { return null; @@ -519,119 +632,15 @@ const ContinueWatchingSection = React.forwardRef((props, re ( - handleContentPress(item.id, item.type)} - onLongPress={() => handleLongPress(item)} - delayLongPress={800} - > - {/* Poster Image */} - - - - {/* Delete Indicator Overlay */} - {deletingItemId === item.id && ( - - - - )} - - - {/* Content Details */} - - - {(() => { - const isUpNext = item.progress === 0; - return ( - - - {item.name} - - {isUpNext && ( - - Up Next - - )} - - ); - })()} - - - {/* Episode Info or Year */} - {(() => { - if (item.type === 'series' && item.season && item.episode) { - return ( - - - Season {item.season} - - {item.episodeTitle && ( - - {item.episodeTitle} - - )} - - ); - } else { - return ( - - {item.year} • {item.type === 'movie' ? 'Movie' : 'Series'} - - ); - } - })()} - - {/* Progress Bar */} - {item.progress > 0 && ( - - - - - - {Math.round(item.progress)}% watched - - - )} - - - )} - keyExtractor={(item) => `continue-${item.id}-${item.type}`} + renderItem={renderContinueWatchingItem} + keyExtractor={keyExtractor} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.wideList} - ItemSeparatorComponent={() => } + ItemSeparatorComponent={ItemSeparator} onEndReachedThreshold={0.7} onEndReached={() => {}} - estimatedItemSize={280 + 16} + removeClippedSubviews={true} /> ); @@ -826,4 +835,7 @@ const styles = StyleSheet.create({ }, }); -export default React.memo(ContinueWatchingSection); \ No newline at end of file +export default React.memo(ContinueWatchingSection, (prevProps, nextProps) => { + // This component has no props that would cause re-renders + return true; +}); \ No newline at end of file diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index a0d337f..0a64c32 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -39,6 +39,7 @@ interface FeaturedContentProps { isSaved: boolean; handleSaveToLibrary: () => void; loading?: boolean; + onRetry?: () => void; } // Cache to store preloaded images @@ -53,7 +54,7 @@ const isTablet = width >= 768; const nowMs = () => Date.now(); const since = (start: number) => `${(nowMs() - start).toFixed(0)}ms`; -const NoFeaturedContent = () => { +const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); @@ -105,29 +106,42 @@ const NoFeaturedContent = () => { return ( - No Featured Content + {onRetry ? 'Couldn\'t load featured content' : 'No Featured Content'} - Install addons with catalogs or change the content source in your settings. + {onRetry + ? 'There was a problem fetching featured content. Please check your connection and try again.' + : 'Install addons with catalogs or change the content source in your settings.'} - navigation.navigate('Addons')} - > - Install Addons - - navigation.navigate('HomeScreenSettings')} - > - Settings - + {onRetry ? ( + + Retry + + ) : ( + <> + navigation.navigate('Addons')} + > + Install Addons + + navigation.navigate('HomeScreenSettings')} + > + Settings + + + )} ); }; -const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading }: FeaturedContentProps) => { +const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading, onRetry }: FeaturedContentProps) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const [bannerUrl, setBannerUrl] = useState(null); @@ -520,7 +534,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin if (!featuredContent) { // Suppress empty state while loading to avoid flash on startup/hydration logger.debug('[FeaturedContent] render:no-featured-content', { sinceMount: since(firstRenderTsRef.current) }); - return ; + return ; } if (isTablet) { diff --git a/src/hooks/useFeaturedContent.ts b/src/hooks/useFeaturedContent.ts index fefaab5..dfc9c9d 100644 --- a/src/hooks/useFeaturedContent.ts +++ b/src/hooks/useFeaturedContent.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback, useRef } from 'react'; +import { AppState } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { StreamingContent, catalogService } from '../services/catalogService'; import { tmdbService } from '../services/tmdbService'; @@ -547,20 +548,45 @@ export function useFeaturedContent() { useEffect(() => { if (allFeaturedContent.length <= 1) return; + let intervalId: NodeJS.Timeout | null = null; + let appState = AppState.currentState; + const rotateContent = () => { currentIndexRef.current = (currentIndexRef.current + 1) % allFeaturedContent.length; if (allFeaturedContent[currentIndexRef.current]) { const newContent = allFeaturedContent[currentIndexRef.current]; setFeaturedContent(newContent); - // Also update the persistent store persistentStore.featuredContent = newContent; } }; - // Further increased rotation interval to 90s to reduce CPU cycles - const intervalId = setInterval(rotateContent, 90000); + const start = () => { + if (!intervalId) intervalId = setInterval(rotateContent, 90000); + }; + const stop = () => { + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } + }; - return () => clearInterval(intervalId); + const handleAppStateChange = (nextState: any) => { + if (appState.match(/inactive|background/) && nextState === 'active') { + start(); + } else if (nextState.match(/inactive|background/)) { + stop(); + } + appState = nextState; + }; + + // Start when mounted and app is active + if (!appState.match(/inactive|background/)) start(); + const sub = AppState.addEventListener('change', handleAppStateChange); + + return () => { + stop(); + sub.remove(); + }; }, [allFeaturedContent]); useEffect(() => { diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index c2e7742..5f26e1c 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -1,8 +1,8 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useMemo } from 'react'; import { NavigationContainer, DefaultTheme as NavigationDefaultTheme, DarkTheme as NavigationDarkTheme, Theme, NavigationProp } from '@react-navigation/native'; import { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack'; import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; -import { useColorScheme, Platform, Animated, StatusBar, TouchableOpacity, View, Text, AppState, Easing, Dimensions, useWindowDimensions } from 'react-native'; +import { useColorScheme, Platform, Animated, StatusBar, TouchableOpacity, View, Text, AppState, Easing, Dimensions } from 'react-native'; import { PaperProvider, MD3DarkTheme, MD3LightTheme, adaptNavigationTheme } from 'react-native-paper'; import type { MD3Theme } from 'react-native-paper'; import type { BottomTabBarProps } from '@react-navigation/bottom-tabs'; @@ -360,9 +360,11 @@ const TabIcon = React.memo(({ focused, color, iconName }: { // Update the TabScreenWrapper component with fixed layout dimensions const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => { - const { width, height } = useWindowDimensions(); - const smallestDimension = Math.min(width, height); - const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); + const isTablet = useMemo(() => { + const { width, height } = Dimensions.get('window'); + const smallestDimension = Math.min(width, height); + return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); + }, []); const insets = useSafeAreaInsets(); // Force consistent status bar settings useEffect(() => { @@ -425,9 +427,11 @@ const WrappedScreen: React.FC<{Screen: React.ComponentType}> = ({ Screen }) const MainTabs = () => { const { currentTheme } = useTheme(); const { isHomeLoading } = useLoading(); - const { width, height } = useWindowDimensions(); - const smallestDimension = Math.min(width, height); - const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); + const isTablet = useMemo(() => { + const { width, height } = Dimensions.get('window'); + const smallestDimension = Math.min(width, height); + return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); + }, []); const insets = useSafeAreaInsets(); const isIosTablet = Platform.OS === 'ios' && isTablet; const [hidden, setHidden] = React.useState(HeaderVisibility.isHidden()); diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 0f14638..9137b58 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -133,34 +133,31 @@ const HomeScreen = () => { refreshFeatured } = useFeaturedContent(); - // Progressive catalog loading function with performance optimizations + // Progressive catalog loading function with performance optimizations const loadCatalogsProgressively = useCallback(async () => { setCatalogsLoading(true); setCatalogs([]); setLoadedCatalogCount(0); try { - const addons = await catalogService.getAllAddons(); + const [addons, catalogSettingsJson, addonManifests] = await Promise.all([ + catalogService.getAllAddons(), + AsyncStorage.getItem(CATALOG_SETTINGS_KEY), + stremioService.getInstalledAddonsAsync() + ]); // Set hasAddons state based on whether we have any addons setHasAddons(addons.length > 0); - // Load catalog settings to check which catalogs are enabled - const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY); const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {}; - // Hoist addon manifest loading out of the loop - const addonManifests = await stremioService.getInstalledAddonsAsync(); - // Create placeholder array with proper order and track indices - const catalogPlaceholders: (CatalogContent | null)[] = []; - const catalogPromises: Promise[] = []; let catalogIndex = 0; + const catalogQueue: (() => Promise)[] = []; // Limit concurrent catalog loading to prevent overwhelming the system const MAX_CONCURRENT_CATALOGS = 1; // Single catalog at a time to minimize heating/memory let activeCatalogLoads = 0; - const catalogQueue: (() => Promise)[] = []; const processCatalogQueue = async () => { while (catalogQueue.length > 0 && activeCatalogLoads < MAX_CONCURRENT_CATALOGS) { @@ -187,7 +184,6 @@ const HomeScreen = () => { // Only load enabled catalogs if (isEnabled) { const currentIndex = catalogIndex; - catalogPlaceholders.push(null); // Reserve position const catalogLoader = async () => { try { @@ -218,7 +214,6 @@ const HomeScreen = () => { certification: meta.certification })); - // Skip prefetching to reduce memory pressure (keep disabled) // Resolve custom display name; if custom exists, use as-is const originalName = catalog.name || catalog.id; let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName); @@ -305,11 +300,13 @@ const HomeScreen = () => { setHomeLoading(isLoading); }, [isLoading, setHomeLoading]); - // React to settings changes + // React to settings changes (memoized to prevent unnecessary effects) + const settingsShowHero = settings.showHeroSection; + const settingsFeaturedSource = settings.featuredContentSource; useEffect(() => { - setShowHeroSection(settings.showHeroSection); - setFeaturedContentSource(settings.featuredContentSource); - }, [settings]); + setShowHeroSection(settingsShowHero); + setFeaturedContentSource(settingsFeaturedSource); + }, [settingsShowHero, settingsFeaturedSource]); // Load catalogs progressively on mount and when settings change useEffect(() => { @@ -362,7 +359,7 @@ const HomeScreen = () => { const unsubscribe = settingsEmitter.addListener(handleSettingsChange); return unsubscribe; - }, [settings]); + }, [settings.showHeroSection, settings.featuredContentSource]); useFocusEffect( useCallback(() => { @@ -374,10 +371,8 @@ const HomeScreen = () => { // Allow free rotation on tablets; lock portrait on phones try { - const { width: dw, height: dh } = Dimensions.get('window'); - const smallestDimension = Math.min(dw, dh); - const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); - if (isTablet) { + const isTabletDevice = Platform.OS === 'ios' ? (Platform as any).isPad === true : isTablet; + if (isTabletDevice) { ScreenOrientation.unlockAsync(); } else { ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); @@ -567,7 +562,8 @@ const HomeScreen = () => { return null; }, [isLoading, currentTheme.colors]); - const listData: HomeScreenListItem[] = useMemo(() => { + // Stabilize listData to prevent FlashList re-renders + const listData = useMemo(() => { const data: HomeScreenListItem[] = []; // If no addons are installed, just show the welcome component @@ -577,7 +573,6 @@ const HomeScreen = () => { } // Normal flow when addons are present (featured moved to ListHeaderComponent) - data.push({ type: 'thisWeek', key: 'thisWeek' }); // Only show a limited number of catalogs initially for performance @@ -594,25 +589,27 @@ const HomeScreen = () => { // Add a "Load More" button if there are more catalogs to show if (catalogs.length > visibleCatalogCount && catalogs.filter(c => c).length > visibleCatalogCount) { - data.push({ type: 'loadMore', key: 'load-more' } as any); + data.push({ type: 'loadMore', key: 'load-more' }); } return data; - }, [hasAddons, showHeroSection, catalogs, visibleCatalogCount]); + }, [hasAddons, catalogs, visibleCatalogCount]); const handleLoadMoreCatalogs = useCallback(() => { setVisibleCatalogCount(prev => Math.min(prev + 3, catalogs.length)); }, [catalogs.length]); - // Add memory cleanup on scroll end - const handleScrollEnd = useCallback(() => { - // No-op; avoid clearing image memory cache here to prevent decode thrash/heating + // Stable keyExtractor for FlashList + const keyExtractor = useCallback((item: HomeScreenListItem) => item.key, []); + + // Memoize device check to avoid repeated Dimensions.get calls + const isTablet = useMemo(() => { + const deviceWidth = Dimensions.get('window').width; + return deviceWidth >= 768; }, []); // Memoize individual section components to prevent re-renders const memoizedFeaturedContent = useMemo(() => { - const deviceWidth = Dimensions.get('window').width; - const isTablet = deviceWidth >= 768; const heroStyleToUse = isTablet ? 'legacy' : settings.heroStyle; return heroStyleToUse === 'carousel' ? ( { loading={featuredLoading} /> ); - }, [settings.heroStyle, showHeroSection, featuredContentSource, featuredContent, allFeaturedContent, isSaved, handleSaveToLibrary]); + }, [isTablet, settings.heroStyle, showHeroSection, featuredContentSource, featuredContent, allFeaturedContent, isSaved, handleSaveToLibrary, featuredLoading]); const memoizedThisWeekSection = useMemo(() => , []); const memoizedContinueWatchingSection = useMemo(() => , []); @@ -649,39 +646,40 @@ const HomeScreen = () => { HeaderVisibility.setHidden(hide); }, []); - const renderListItem = useCallback(({ item, index }: { item: HomeScreenListItem, index: number }) => { - const wrapper = (child: React.ReactNode) => ( - - {child} - - ); + // Stabilize renderItem to prevent FlashList re-renders + const renderListItem = useCallback(({ item }: { item: HomeScreenListItem; index: number }) => { switch (item.type) { - // featured is rendered via ListHeaderComponent to avoid remounts case 'thisWeek': - return wrapper(memoizedThisWeekSection); + return {memoizedThisWeekSection}; case 'continueWatching': return null; // Moved to ListHeaderComponent to avoid remounts on scroll case 'catalog': - return wrapper(); + return ( + + + + ); case 'placeholder': - return wrapper( - - - - + return ( + + + + + + + + {[...Array(3)].map((_, posterIndex) => ( + + ))} + - - {[...Array(3)].map((_, posterIndex) => ( - - ))} - - + ); case 'loadMore': return ( @@ -700,19 +698,11 @@ const HomeScreen = () => { ); case 'welcome': - return wrapper(); + return ; default: return null; } - }, [ - showHeroSection, - featuredContentSource, - featuredContent, - isSaved, - handleSaveToLibrary, - currentTheme.colors, - handleLoadMoreCatalogs - ]); + }, [memoizedThisWeekSection, currentTheme.colors.elevation1, currentTheme.colors.primary, currentTheme.colors.white, handleLoadMoreCatalogs]); // FlashList: using minimal props per installed version @@ -737,6 +727,29 @@ const HomeScreen = () => { ), [catalogsLoading, catalogs, loadedCatalogCount, totalCatalogsRef.current, navigation, currentTheme.colors]); + // Memoize scroll handler to prevent recreating on every render + const handleScroll = useCallback((event: any) => { + const y = event.nativeEvent.contentOffset.y; + const dy = y - lastScrollYRef.current; + lastScrollYRef.current = y; + if (y <= 10) { + toggleHeader(false); + return; + } + // Threshold to avoid jitter + if (dy > 6) { + toggleHeader(true); // scrolling down + } else if (dy < -6) { + toggleHeader(false); // scrolling up + } + }, [toggleHeader]); + + // Memoize content container style + const contentContainerStyle = useMemo(() => + StyleSheet.flatten([styles.scrollContent, { paddingTop: insets.top }]), + [insets.top] + ); + // Memoize the main content section const renderMainContent = useMemo(() => { if (isLoading) return null; @@ -751,42 +764,31 @@ const HomeScreen = () => { item.key} - contentContainerStyle={StyleSheet.flatten([ - styles.scrollContent, - { paddingTop: insets.top } - ])} + keyExtractor={keyExtractor} + contentContainerStyle={contentContainerStyle} showsVerticalScrollIndicator={false} ListHeaderComponent={memoizedHeader} ListFooterComponent={ListFooterComponent} onEndReached={handleLoadMoreCatalogs} onEndReachedThreshold={0.6} - scrollEventThrottle={32} - onScroll={event => { - const y = event.nativeEvent.contentOffset.y; - const dy = y - lastScrollYRef.current; - lastScrollYRef.current = y; - if (y <= 10) { - toggleHeader(false); - return; - } - // Threshold to avoid jitter - if (dy > 6) { - toggleHeader(true); // scrolling down - } else if (dy < -6) { - toggleHeader(false); // scrolling up - } - }} + removeClippedSubviews={true} + scrollEventThrottle={64} + onScroll={handleScroll} /> {/* Toasts are rendered globally at root */} ); }, [ isLoading, - currentTheme.colors, + currentTheme.colors.darkBackground, listData, renderListItem, - ListFooterComponent + keyExtractor, + contentContainerStyle, + memoizedHeader, + ListFooterComponent, + handleLoadMoreCatalogs, + handleScroll ]); return isLoading ? renderLoadingScreen : renderMainContent; diff --git a/src/screens/UpdateScreen.tsx b/src/screens/UpdateScreen.tsx index c7fca3f..cbb82e6 100644 --- a/src/screens/UpdateScreen.tsx +++ b/src/screens/UpdateScreen.tsx @@ -9,8 +9,7 @@ import { StatusBar, Alert, Platform, - Dimensions, - Clipboard + Dimensions } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -70,8 +69,7 @@ const UpdateScreen: React.FC = () => { const [isChecking, setIsChecking] = useState(false); const [isInstalling, setIsInstalling] = useState(false); const [lastChecked, setLastChecked] = useState(null); - const [logs, setLogs] = useState([]); - const [showLogs, setShowLogs] = useState(false); + // Logs removed const [lastOperation, setLastOperation] = useState(''); const [updateProgress, setUpdateProgress] = useState(0); const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'downloading' | 'installing' | 'success' | 'error'>('idle'); @@ -87,9 +85,7 @@ const UpdateScreen: React.FC = () => { setUpdateInfo(info); setLastChecked(new Date()); - // Refresh logs after operation - const logs = UpdateService.getLogs(); - setLogs(logs); + // Logs disabled if (info.isAvailable) { setUpdateStatus('available'); @@ -130,9 +126,7 @@ const UpdateScreen: React.FC = () => { setUpdateStatus('installing'); setLastOperation('Installing update...'); - // Refresh logs after operation - const logs = UpdateService.getLogs(); - setLogs(logs); + // Logs disabled if (success) { setUpdateStatus('success'); @@ -156,8 +150,7 @@ const UpdateScreen: React.FC = () => { const getCurrentUpdateInfo = async () => { const info = await UpdateService.getCurrentUpdateInfo(); setCurrentInfo(info); - const logs = UpdateService.getLogs(); - setLogs(logs); + // Logs disabled }; // Extract release notes from various possible manifest fields @@ -184,41 +177,12 @@ const UpdateScreen: React.FC = () => { ); }; - const refreshLogs = () => { - const logs = UpdateService.getLogs(); - setLogs(logs); - }; - - const clearLogs = () => { - UpdateService.clearLogs(); - setLogs([]); - setLastOperation('Logs cleared'); - }; - - const copyLog = (logText: string) => { - Clipboard.setString(logText); - Alert.alert('Copied', 'Log entry copied to clipboard'); - }; - - const copyAllLogs = () => { - const allLogsText = logs.join('\n'); - Clipboard.setString(allLogsText); - Alert.alert('Copied', 'All logs copied to clipboard'); - }; - - const addTestLog = () => { - UpdateService.addTestLog(`Test log entry at ${new Date().toISOString()}`); - const logs = UpdateService.getLogs(); - setLogs(logs); - setLastOperation('Test log added'); - }; + // Logs disabled: remove actions const testConnectivity = async () => { try { setLastOperation('Testing connectivity...'); const isReachable = await UpdateService.testUpdateConnectivity(); - const logs = UpdateService.getLogs(); - setLogs(logs); if (isReachable) { setLastOperation('Update server is reachable'); @@ -228,8 +192,7 @@ const UpdateScreen: React.FC = () => { } catch (error) { if (__DEV__) console.error('Error testing connectivity:', error); setLastOperation(`Connectivity test error: ${error instanceof Error ? error.message : 'Unknown error'}`); - const logs = UpdateService.getLogs(); - setLogs(logs); + // Logs disabled } }; @@ -237,14 +200,11 @@ const UpdateScreen: React.FC = () => { try { setLastOperation('Testing asset URLs...'); await UpdateService.testAllAssetUrls(); - const logs = UpdateService.getLogs(); - setLogs(logs); setLastOperation('Asset URL testing completed'); } catch (error) { if (__DEV__) console.error('Error testing asset URLs:', error); setLastOperation(`Asset URL test error: ${error instanceof Error ? error.message : 'Unknown error'}`); - const logs = UpdateService.getLogs(); - setLogs(logs); + // Logs disabled } }; @@ -252,8 +212,6 @@ const UpdateScreen: React.FC = () => { useEffect(() => { const loadInitialData = async () => { await getCurrentUpdateInfo(); - // Also refresh logs to ensure we have the latest - refreshLogs(); }; loadInitialData(); }, []); @@ -505,30 +463,10 @@ const UpdateScreen: React.FC = () => { )} - {/* Advanced Toggle */} - setShowLogs(!showLogs)} - activeOpacity={0.7} - > - - - - Developer Logs - - - {logs.length} - - - - + {/* Developer Logs removed */} - {showLogs && ( + {false && ( @@ -550,34 +488,10 @@ const UpdateScreen: React.FC = () => { > - - - - - - - - - - - - + {/* Test log removed */} + {/* Copy all logs removed */} + {/* Refresh logs removed */} + {/* Clear logs removed */} @@ -586,14 +500,12 @@ const UpdateScreen: React.FC = () => { showsVerticalScrollIndicator={true} nestedScrollEnabled={true} > - {logs.length === 0 ? ( - - No logs available - + {false ? ( + No logs available ) : ( - logs.map((log, index) => { - const isError = log.includes('[ERROR]'); - const isWarning = log.includes('[WARN]'); + ([] as string[]).map((log, index) => { + const isError = log.indexOf('[ERROR]') !== -1; + const isWarning = log.indexOf('[WARN]') !== -1; return ( { styles.logEntry, { backgroundColor: 'rgba(255,255,255,0.05)' } ]} - onPress={() => copyLog(log)} + onPress={() => {}} activeOpacity={0.7} > diff --git a/src/services/updateService.ts b/src/services/updateService.ts index 0df9297..9116883 100644 --- a/src/services/updateService.ts +++ b/src/services/updateService.ts @@ -28,61 +28,32 @@ export class UpdateService { * Add a log entry with timestamp - always log to console for adb logcat visibility */ private addLog(message: string, level: 'INFO' | 'WARN' | 'ERROR' = 'INFO'): void { - const timestamp = new Date().toISOString(); - const logEntry = `[${timestamp}] [${level}] ${message}`; - - this.logs.unshift(logEntry); - - // Keep only the last MAX_LOGS entries - if (this.logs.length > this.MAX_LOGS) { - this.logs = this.logs.slice(0, this.MAX_LOGS); - } - - // Console logging policy: - // - Development: log INFO/WARN/ERROR for visibility - // - Production: only log ERROR to reduce JS<->native bridge traffic and CPU usage - if (!__DEV__) { - if (level === 'ERROR') { - console.error(`[UpdateService] ${logEntry}`); - } - return; - } - - // Development detailed logging - if (level === 'ERROR') { - console.error(`[UpdateService] ${logEntry}`); - } else if (level === 'WARN') { - console.warn(`[UpdateService] ${logEntry}`); - } else { - console.log(`[UpdateService] ${logEntry}`); - } - - // Additional prefixed line for easier filtering during development only - if (__DEV__) { - console.log(`UpdateService: ${logEntry}`); - } + // Logging disabled intentionally + return; } /** * Get all logs */ public getLogs(): string[] { - return [...this.logs]; + // Logging disabled - return empty list + return []; } /** * Clear all logs */ public clearLogs(): void { + // Logging disabled - no-op this.logs = []; - this.addLog('Logs cleared', 'INFO'); } /** * Add a test log entry (useful for debugging) */ public addTestLog(message: string): void { - this.addLog(`TEST: ${message}`, 'INFO'); + // Logging disabled - no-op + return; } /**