potential heat fix

This commit is contained in:
tapframe 2025-09-10 21:38:43 +05:30
parent 097073fcd3
commit bc2a15f81f
12 changed files with 386 additions and 405 deletions

View file

@ -59,6 +59,14 @@ enableScreens(true);
// Inner app component that uses the theme context // Inner app component that uses the theme context
const ThemedApp = () => { 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 { currentTheme } = useTheme();
const [isAppReady, setIsAppReady] = useState(false); const [isAppReady, setIsAppReady] = useState(false);
const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState<boolean | null>(null); const [hasCompletedOnboarding, setHasCompletedOnboarding] = useState<boolean | null>(null);

View file

@ -16,7 +16,7 @@
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/> <meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/> <meta-data android:name="expo.modules.updates.EXPO_RUNTIME_VERSION" android:value="@string/expo_runtime_version"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="30000"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"/> <meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified"> <activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="unspecified">
<intent-filter> <intent-filter>

View file

@ -20,7 +20,7 @@ interface UpdatePopupProps {
updateInfo: { updateInfo: {
isAvailable: boolean; isAvailable: boolean;
manifest?: { manifest?: {
id: string; id?: string;
version?: string; version?: string;
description?: string; description?: string;
}; };

View file

@ -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 { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions } from 'react-native';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { NavigationProp, useNavigation } from '@react-navigation/native'; import { NavigationProp, useNavigation } from '@react-navigation/native';
@ -56,23 +56,49 @@ const POSTER_WIDTH = posterLayout.posterWidth;
const CatalogSection = ({ catalog }: CatalogSectionProps) => { const CatalogSection = ({ catalog }: CatalogSectionProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
// Simplified visibility tracking to reduce state updates and re-renders
const [visibleIndexSet, setVisibleIndexSet] = useState<Set<number>>(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<number>();
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) => { const handleContentPress = useCallback((id: string, type: string) => {
navigation.navigate('Metadata', { id, type, addonId: catalog.addon }); navigation.navigate('Metadata', { id, type, addonId: catalog.addon });
}, [navigation, catalog.addon]); }, [navigation, catalog.addon]);
const renderContentItem = useCallback(({ item, index }: { item: StreamingContent, index: number }) => { const renderContentItem = useCallback(({ item, index }: { item: StreamingContent, index: number }) => {
// Only load images for the first few items eagerly; others defer based on viewability // Simplify visibility logic to reduce re-renders
const eager = index < 6; const isVisible = visibleIndexSet.has(index) || index < 8;
return ( return (
<ContentItem <ContentItem
item={item} item={item}
onPress={handleContentPress} onPress={handleContentPress}
shouldLoadImage={eager} shouldLoadImage={isVisible}
deferMs={eager ? 0 : Math.min(400 + index * 15, 1500)} deferMs={0}
/> />
); );
}, [handleContentPress]); }, [handleContentPress, visibleIndexSet]);
// Memoize the ItemSeparatorComponent to prevent re-creation // Memoize the ItemSeparatorComponent to prevent re-creation
const ItemSeparator = useCallback(() => <View style={{ width: 8 }} />, []); const ItemSeparator = useCallback(() => <View style={{ width: 8 }} />, []);
@ -112,7 +138,10 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
ItemSeparatorComponent={ItemSeparator} ItemSeparatorComponent={ItemSeparator}
onEndReachedThreshold={0.7} onEndReachedThreshold={0.7}
onEndReached={() => {}} onEndReached={() => {}}
scrollEventThrottle={32} scrollEventThrottle={64}
viewabilityConfig={viewabilityConfig as any}
onViewableItemsChanged={onViewableItemsChanged.current as any}
removeClippedSubviews={true}
/> />
</Animated.View> </Animated.View>
); );

View file

@ -68,6 +68,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings } = useSettings(); const { settings } = useSettings();
const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12; const posterRadius = typeof settings.posterBorderRadius === 'number' ? settings.posterBorderRadius : 12;
// Memoize poster width calculation to avoid recalculating on every render
const posterWidth = React.useMemo(() => { const posterWidth = React.useMemo(() => {
switch (settings.posterSize) { switch (settings.posterSize) {
case 'small': case 'small':
@ -130,31 +131,31 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [shouldLoadImageProp, deferMs]); }, [shouldLoadImageProp, deferMs]);
// Get optimized poster URL for smaller tiles // Memoize optimized poster URL to prevent recalculating
const getOptimizedPosterUrl = useCallback((originalUrl: string) => { const optimizedPosterUrl = React.useMemo(() => {
if (!originalUrl || originalUrl.includes('placeholder')) { if (!item.poster || item.poster.includes('placeholder')) {
return 'https://via.placeholder.com/154x231/333/666?text=No+Image'; return 'https://via.placeholder.com/154x231/333/666?text=No+Image';
} }
// If we've had an error, try metahub fallback // 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`; return `https://images.metahub.space/poster/small/${item.id}/img`;
} }
// For TMDB images, use smaller sizes // 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) // 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 // For metahub images, use smaller sizes
if (originalUrl.includes('images.metahub.space')) { if (item.poster.includes('images.metahub.space')) {
return originalUrl.replace('/medium/', '/small/'); return item.poster.replace('/medium/', '/small/');
} }
// Return original URL for other sources to avoid breaking them // Return original URL for other sources to avoid breaking them
return originalUrl; return item.poster;
}, [retryCount, item.id]); }, [item.poster, retryCount, item.id]);
return ( return (
<> <>
@ -170,7 +171,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
{/* Only load image when shouldLoadImage is true (lazy loading) */} {/* Only load image when shouldLoadImage is true (lazy loading) */}
{(shouldLoadImageProp ?? shouldLoadImageState) && item.poster ? ( {(shouldLoadImageProp ?? shouldLoadImageState) && item.poster ? (
<ExpoImage <ExpoImage
source={{ uri: getOptimizedPosterUrl(item.poster) }} source={{ uri: optimizedPosterUrl }}
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]} style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]}
contentFit="cover" contentFit="cover"
cachePolicy={Platform.OS === 'android' ? 'disk' : 'memory-disk'} cachePolicy={Platform.OS === 'android' ? 'disk' : 'memory-disk'}
@ -303,7 +304,9 @@ const styles = StyleSheet.create({
}); });
export default React.memo(ContentItem, (prev, next) => { export default React.memo(ContentItem, (prev, next) => {
// Aggressive memoization - only re-render if ID changes (different item entirely) // Re-render when identity changes or when visibility-driven loading flips
// This keeps loaded posters stable during fast scrolls if (prev.item.id !== next.item.id) return false;
return prev.item.id === next.item.id; if (prev.item.poster !== next.item.poster) return false;
if ((prev.shouldLoadImage ?? false) !== (next.shouldLoadImage ?? false)) return false;
return true;
}); });

View file

@ -96,8 +96,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
const [deletingItemId, setDeletingItemId] = useState<string | null>(null); const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(null); const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Use a state to track if a background refresh is in progress // Use a ref to track if a background refresh is in progress to avoid state updates
const [isRefreshing, setIsRefreshing] = useState(false); const isRefreshingRef = useRef(false);
// Cache for metadata to avoid redundant API calls // Cache for metadata to avoid redundant API calls
const metadataCache = useRef<Record<string, { metadata: any; basicContent: StreamingContent | null; timestamp: number }>>({}); const metadataCache = useRef<Record<string, { metadata: any; basicContent: StreamingContent | null; timestamp: number }>>({});
@ -133,12 +133,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
// Modified loadContinueWatching to render incrementally // Modified loadContinueWatching to render incrementally
const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => { const loadContinueWatching = useCallback(async (isBackgroundRefresh = false) => {
if (isRefreshing) return; if (isRefreshingRef.current) return;
if (!isBackgroundRefresh) { if (!isBackgroundRefresh) {
setLoading(true); setLoading(true);
} }
setIsRefreshing(true); isRefreshingRef.current = true;
// Helper to merge a batch of items into state (dedupe by type:id, keep newest) // Helper to merge a batch of items into state (dedupe by type:id, keep newest)
const mergeBatchIntoState = (batch: ContinueWatchingItem[]) => { const mergeBatchIntoState = (batch: ContinueWatchingItem[]) => {
@ -361,9 +361,9 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
logger.error('Failed to load continue watching items:', error); logger.error('Failed to load continue watching items:', error);
} finally { } finally {
setLoading(false); setLoading(false);
setIsRefreshing(false); isRefreshingRef.current = false;
} }
}, [isRefreshing, getCachedMetadata]); }, [getCachedMetadata]);
// Clear cache when component unmounts or when needed // Clear cache when component unmounts or when needed
useEffect(() => { useEffect(() => {
@ -398,7 +398,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
refreshTimerRef.current = setTimeout(() => { refreshTimerRef.current = setTimeout(() => {
// Trigger a background refresh // Trigger a background refresh
loadContinueWatching(true); 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 // Try to set up a custom event listener or use a timer as fallback
@ -415,8 +415,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
} }
}; };
} else { } else {
// Reduced polling frequency from 30s to 2 minutes to reduce heating // Reduced polling frequency from 30s to 5 minutes to reduce heating and battery drain
const intervalId = setInterval(() => loadContinueWatching(true), 120000); const intervalId = setInterval(() => loadContinueWatching(true), 300000);
return () => { return () => {
subscription.remove(); subscription.remove();
clearInterval(intervalId); clearInterval(intervalId);
@ -448,7 +448,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
navigation.navigate('Metadata', { id, type }); navigation.navigate('Metadata', { id, type });
}, [navigation]); }, [navigation]);
// Handle long press to delete // Handle long press to delete (moved before renderContinueWatchingItem)
const handleLongPress = useCallback((item: ContinueWatchingItem) => { const handleLongPress = useCallback((item: ContinueWatchingItem) => {
try { try {
// Trigger haptic feedback // Trigger haptic feedback
@ -503,6 +503,119 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
); );
}, []); }, []);
// Memoized render function for continue watching items
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
<TouchableOpacity
style={[styles.wideContentItem, {
backgroundColor: currentTheme.colors.elevation1,
borderColor: currentTheme.colors.border,
shadowColor: currentTheme.colors.black
}]}
activeOpacity={0.8}
onPress={() => handleContentPress(item.id, item.type)}
onLongPress={() => handleLongPress(item)}
delayLongPress={800}
>
{/* Poster Image */}
<View style={styles.posterContainer}>
<ExpoImage
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.continueWatchingPoster}
contentFit="cover"
cachePolicy="memory"
transition={0}
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
/>
{/* Delete Indicator Overlay */}
{deletingItemId === item.id && (
<View style={styles.deletingOverlay}>
<ActivityIndicator size="large" color="#FFFFFF" />
</View>
)}
</View>
{/* Content Details */}
<View style={styles.contentDetails}>
<View style={styles.titleRow}>
{(() => {
const isUpNext = item.progress === 0;
return (
<View style={styles.titleRow}>
<Text
style={[styles.contentTitle, { color: currentTheme.colors.highEmphasis }]}
numberOfLines={1}
>
{item.name}
</Text>
{isUpNext && (
<View style={[styles.progressBadge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.progressText}>Up Next</Text>
</View>
)}
</View>
);
})()}
</View>
{/* Episode Info or Year */}
{(() => {
if (item.type === 'series' && item.season && item.episode) {
return (
<View style={styles.episodeRow}>
<Text style={[styles.episodeText, { color: currentTheme.colors.mediumEmphasis }]}>
Season {item.season}
</Text>
{item.episodeTitle && (
<Text
style={[styles.episodeTitle, { color: currentTheme.colors.mediumEmphasis }]}
numberOfLines={1}
>
{item.episodeTitle}
</Text>
)}
</View>
);
} else {
return (
<Text style={[styles.yearText, { color: currentTheme.colors.mediumEmphasis }]}>
{item.year} {item.type === 'movie' ? 'Movie' : 'Series'}
</Text>
);
}
})()}
{/* Progress Bar */}
{item.progress > 0 && (
<View style={styles.wideProgressContainer}>
<View style={styles.wideProgressTrack}>
<View
style={[
styles.wideProgressBar,
{
width: `${item.progress}%`,
backgroundColor: currentTheme.colors.primary
}
]}
/>
</View>
<Text style={[styles.progressLabel, { color: currentTheme.colors.textMuted }]}>
{Math.round(item.progress)}% watched
</Text>
</View>
)}
</View>
</TouchableOpacity>
), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId]);
// Memoized key extractor
const keyExtractor = useCallback((item: ContinueWatchingItem) => `continue-${item.id}-${item.type}`, []);
// Memoized item separator
const ItemSeparator = useCallback(() => <View style={{ width: 16 }} />, []);
// If no continue watching items, don't render anything // If no continue watching items, don't render anything
if (continueWatchingItems.length === 0) { if (continueWatchingItems.length === 0) {
return null; return null;
@ -519,119 +632,15 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
<FlashList <FlashList
data={continueWatchingItems} data={continueWatchingItems}
renderItem={({ item }) => ( renderItem={renderContinueWatchingItem}
<TouchableOpacity keyExtractor={keyExtractor}
style={[styles.wideContentItem, {
backgroundColor: currentTheme.colors.elevation1,
borderColor: currentTheme.colors.border,
shadowColor: currentTheme.colors.black
}]}
activeOpacity={0.8}
onPress={() => handleContentPress(item.id, item.type)}
onLongPress={() => handleLongPress(item)}
delayLongPress={800}
>
{/* Poster Image */}
<View style={styles.posterContainer}>
<ExpoImage
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.continueWatchingPoster}
contentFit="cover"
cachePolicy="memory"
transition={0}
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
placeholderContentFit="cover"
recyclingKey={item.id}
/>
{/* Delete Indicator Overlay */}
{deletingItemId === item.id && (
<View style={styles.deletingOverlay}>
<ActivityIndicator size="large" color="#FFFFFF" />
</View>
)}
</View>
{/* Content Details */}
<View style={styles.contentDetails}>
<View style={styles.titleRow}>
{(() => {
const isUpNext = item.progress === 0;
return (
<View style={styles.titleRow}>
<Text
style={[styles.contentTitle, { color: currentTheme.colors.highEmphasis }]}
numberOfLines={1}
>
{item.name}
</Text>
{isUpNext && (
<View style={[styles.progressBadge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.progressText}>Up Next</Text>
</View>
)}
</View>
);
})()}
</View>
{/* Episode Info or Year */}
{(() => {
if (item.type === 'series' && item.season && item.episode) {
return (
<View style={styles.episodeRow}>
<Text style={[styles.episodeText, { color: currentTheme.colors.mediumEmphasis }]}>
Season {item.season}
</Text>
{item.episodeTitle && (
<Text
style={[styles.episodeTitle, { color: currentTheme.colors.mediumEmphasis }]}
numberOfLines={1}
>
{item.episodeTitle}
</Text>
)}
</View>
);
} else {
return (
<Text style={[styles.yearText, { color: currentTheme.colors.mediumEmphasis }]}>
{item.year} {item.type === 'movie' ? 'Movie' : 'Series'}
</Text>
);
}
})()}
{/* Progress Bar */}
{item.progress > 0 && (
<View style={styles.wideProgressContainer}>
<View style={styles.wideProgressTrack}>
<View
style={[
styles.wideProgressBar,
{
width: `${item.progress}%`,
backgroundColor: currentTheme.colors.primary
}
]}
/>
</View>
<Text style={[styles.progressLabel, { color: currentTheme.colors.textMuted }]}>
{Math.round(item.progress)}% watched
</Text>
</View>
)}
</View>
</TouchableOpacity>
)}
keyExtractor={(item) => `continue-${item.id}-${item.type}`}
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.wideList} contentContainerStyle={styles.wideList}
ItemSeparatorComponent={() => <View style={{ width: 16 }} />} ItemSeparatorComponent={ItemSeparator}
onEndReachedThreshold={0.7} onEndReachedThreshold={0.7}
onEndReached={() => {}} onEndReached={() => {}}
estimatedItemSize={280 + 16} removeClippedSubviews={true}
/> />
</Animated.View> </Animated.View>
); );
@ -826,4 +835,7 @@ const styles = StyleSheet.create({
}, },
}); });
export default React.memo(ContinueWatchingSection); export default React.memo(ContinueWatchingSection, (prevProps, nextProps) => {
// This component has no props that would cause re-renders
return true;
});

View file

@ -39,6 +39,7 @@ interface FeaturedContentProps {
isSaved: boolean; isSaved: boolean;
handleSaveToLibrary: () => void; handleSaveToLibrary: () => void;
loading?: boolean; loading?: boolean;
onRetry?: () => void;
} }
// Cache to store preloaded images // Cache to store preloaded images
@ -53,7 +54,7 @@ const isTablet = width >= 768;
const nowMs = () => Date.now(); const nowMs = () => Date.now();
const since = (start: number) => `${(nowMs() - start).toFixed(0)}ms`; const since = (start: number) => `${(nowMs() - start).toFixed(0)}ms`;
const NoFeaturedContent = () => { const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
@ -105,29 +106,42 @@ const NoFeaturedContent = () => {
return ( return (
<View style={styles.noContentContainer}> <View style={styles.noContentContainer}>
<MaterialIcons name="theaters" size={48} color={currentTheme.colors.mediumEmphasis} /> <MaterialIcons name="theaters" size={48} color={currentTheme.colors.mediumEmphasis} />
<Text style={styles.noContentTitle}>No Featured Content</Text> <Text style={styles.noContentTitle}>{onRetry ? 'Couldn\'t load featured content' : 'No Featured Content'}</Text>
<Text style={styles.noContentText}> <Text style={styles.noContentText}>
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.'}
</Text> </Text>
<View style={styles.noContentButtons}> <View style={styles.noContentButtons}>
<TouchableOpacity {onRetry ? (
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]} <TouchableOpacity
onPress={() => navigation.navigate('Addons')} style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
> onPress={onRetry}
<Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Install Addons</Text> >
</TouchableOpacity> <Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Retry</Text>
<TouchableOpacity </TouchableOpacity>
style={styles.noContentButton} ) : (
onPress={() => navigation.navigate('HomeScreenSettings')} <>
> <TouchableOpacity
<Text style={styles.noContentButtonText}>Settings</Text> style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
</TouchableOpacity> onPress={() => navigation.navigate('Addons')}
>
<Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Install Addons</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.noContentButton}
onPress={() => navigation.navigate('HomeScreenSettings')}
>
<Text style={styles.noContentButtonText}>Settings</Text>
</TouchableOpacity>
</>
)}
</View> </View>
</View> </View>
); );
}; };
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading }: FeaturedContentProps) => { const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading, onRetry }: FeaturedContentProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const [bannerUrl, setBannerUrl] = useState<string | null>(null); const [bannerUrl, setBannerUrl] = useState<string | null>(null);
@ -520,7 +534,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
if (!featuredContent) { if (!featuredContent) {
// Suppress empty state while loading to avoid flash on startup/hydration // Suppress empty state while loading to avoid flash on startup/hydration
logger.debug('[FeaturedContent] render:no-featured-content', { sinceMount: since(firstRenderTsRef.current) }); logger.debug('[FeaturedContent] render:no-featured-content', { sinceMount: since(firstRenderTsRef.current) });
return <NoFeaturedContent />; return <NoFeaturedContent onRetry={onRetry} />;
} }
if (isTablet) { if (isTablet) {

View file

@ -1,4 +1,5 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { AppState } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { StreamingContent, catalogService } from '../services/catalogService'; import { StreamingContent, catalogService } from '../services/catalogService';
import { tmdbService } from '../services/tmdbService'; import { tmdbService } from '../services/tmdbService';
@ -547,20 +548,45 @@ export function useFeaturedContent() {
useEffect(() => { useEffect(() => {
if (allFeaturedContent.length <= 1) return; if (allFeaturedContent.length <= 1) return;
let intervalId: NodeJS.Timeout | null = null;
let appState = AppState.currentState;
const rotateContent = () => { const rotateContent = () => {
currentIndexRef.current = (currentIndexRef.current + 1) % allFeaturedContent.length; currentIndexRef.current = (currentIndexRef.current + 1) % allFeaturedContent.length;
if (allFeaturedContent[currentIndexRef.current]) { if (allFeaturedContent[currentIndexRef.current]) {
const newContent = allFeaturedContent[currentIndexRef.current]; const newContent = allFeaturedContent[currentIndexRef.current];
setFeaturedContent(newContent); setFeaturedContent(newContent);
// Also update the persistent store
persistentStore.featuredContent = newContent; persistentStore.featuredContent = newContent;
} }
}; };
// Further increased rotation interval to 90s to reduce CPU cycles const start = () => {
const intervalId = setInterval(rotateContent, 90000); 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]); }, [allFeaturedContent]);
useEffect(() => { useEffect(() => {

View file

@ -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 { NavigationContainer, DefaultTheme as NavigationDefaultTheme, DarkTheme as NavigationDarkTheme, Theme, NavigationProp } from '@react-navigation/native';
import { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack'; import { createNativeStackNavigator, NativeStackNavigationOptions, NativeStackNavigationProp } from '@react-navigation/native-stack';
import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; 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 { PaperProvider, MD3DarkTheme, MD3LightTheme, adaptNavigationTheme } from 'react-native-paper';
import type { MD3Theme } from 'react-native-paper'; import type { MD3Theme } from 'react-native-paper';
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs'; 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 // Update the TabScreenWrapper component with fixed layout dimensions
const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => { const TabScreenWrapper: React.FC<{children: React.ReactNode}> = ({ children }) => {
const { width, height } = useWindowDimensions(); const isTablet = useMemo(() => {
const smallestDimension = Math.min(width, height); const { width, height } = Dimensions.get('window');
const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); const smallestDimension = Math.min(width, height);
return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
}, []);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
// Force consistent status bar settings // Force consistent status bar settings
useEffect(() => { useEffect(() => {
@ -425,9 +427,11 @@ const WrappedScreen: React.FC<{Screen: React.ComponentType<any>}> = ({ Screen })
const MainTabs = () => { const MainTabs = () => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { isHomeLoading } = useLoading(); const { isHomeLoading } = useLoading();
const { width, height } = useWindowDimensions(); const isTablet = useMemo(() => {
const smallestDimension = Math.min(width, height); const { width, height } = Dimensions.get('window');
const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768); const smallestDimension = Math.min(width, height);
return (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
}, []);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const isIosTablet = Platform.OS === 'ios' && isTablet; const isIosTablet = Platform.OS === 'ios' && isTablet;
const [hidden, setHidden] = React.useState(HeaderVisibility.isHidden()); const [hidden, setHidden] = React.useState(HeaderVisibility.isHidden());

View file

@ -133,34 +133,31 @@ const HomeScreen = () => {
refreshFeatured refreshFeatured
} = useFeaturedContent(); } = useFeaturedContent();
// Progressive catalog loading function with performance optimizations // Progressive catalog loading function with performance optimizations
const loadCatalogsProgressively = useCallback(async () => { const loadCatalogsProgressively = useCallback(async () => {
setCatalogsLoading(true); setCatalogsLoading(true);
setCatalogs([]); setCatalogs([]);
setLoadedCatalogCount(0); setLoadedCatalogCount(0);
try { 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 // Set hasAddons state based on whether we have any addons
setHasAddons(addons.length > 0); 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) : {}; 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 // Create placeholder array with proper order and track indices
const catalogPlaceholders: (CatalogContent | null)[] = [];
const catalogPromises: Promise<void>[] = [];
let catalogIndex = 0; let catalogIndex = 0;
const catalogQueue: (() => Promise<void>)[] = [];
// Limit concurrent catalog loading to prevent overwhelming the system // Limit concurrent catalog loading to prevent overwhelming the system
const MAX_CONCURRENT_CATALOGS = 1; // Single catalog at a time to minimize heating/memory const MAX_CONCURRENT_CATALOGS = 1; // Single catalog at a time to minimize heating/memory
let activeCatalogLoads = 0; let activeCatalogLoads = 0;
const catalogQueue: (() => Promise<void>)[] = [];
const processCatalogQueue = async () => { const processCatalogQueue = async () => {
while (catalogQueue.length > 0 && activeCatalogLoads < MAX_CONCURRENT_CATALOGS) { while (catalogQueue.length > 0 && activeCatalogLoads < MAX_CONCURRENT_CATALOGS) {
@ -187,7 +184,6 @@ const HomeScreen = () => {
// Only load enabled catalogs // Only load enabled catalogs
if (isEnabled) { if (isEnabled) {
const currentIndex = catalogIndex; const currentIndex = catalogIndex;
catalogPlaceholders.push(null); // Reserve position
const catalogLoader = async () => { const catalogLoader = async () => {
try { try {
@ -218,7 +214,6 @@ const HomeScreen = () => {
certification: meta.certification certification: meta.certification
})); }));
// Skip prefetching to reduce memory pressure (keep disabled)
// Resolve custom display name; if custom exists, use as-is // Resolve custom display name; if custom exists, use as-is
const originalName = catalog.name || catalog.id; const originalName = catalog.name || catalog.id;
let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName); let displayName = await getCatalogDisplayName(addon.id, catalog.type, catalog.id, originalName);
@ -305,11 +300,13 @@ const HomeScreen = () => {
setHomeLoading(isLoading); setHomeLoading(isLoading);
}, [isLoading, setHomeLoading]); }, [isLoading, setHomeLoading]);
// React to settings changes // React to settings changes (memoized to prevent unnecessary effects)
const settingsShowHero = settings.showHeroSection;
const settingsFeaturedSource = settings.featuredContentSource;
useEffect(() => { useEffect(() => {
setShowHeroSection(settings.showHeroSection); setShowHeroSection(settingsShowHero);
setFeaturedContentSource(settings.featuredContentSource); setFeaturedContentSource(settingsFeaturedSource);
}, [settings]); }, [settingsShowHero, settingsFeaturedSource]);
// Load catalogs progressively on mount and when settings change // Load catalogs progressively on mount and when settings change
useEffect(() => { useEffect(() => {
@ -362,7 +359,7 @@ const HomeScreen = () => {
const unsubscribe = settingsEmitter.addListener(handleSettingsChange); const unsubscribe = settingsEmitter.addListener(handleSettingsChange);
return unsubscribe; return unsubscribe;
}, [settings]); }, [settings.showHeroSection, settings.featuredContentSource]);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@ -374,10 +371,8 @@ const HomeScreen = () => {
// Allow free rotation on tablets; lock portrait on phones // Allow free rotation on tablets; lock portrait on phones
try { try {
const { width: dw, height: dh } = Dimensions.get('window'); const isTabletDevice = Platform.OS === 'ios' ? (Platform as any).isPad === true : isTablet;
const smallestDimension = Math.min(dw, dh); if (isTabletDevice) {
const isTablet = (Platform.OS === 'ios' ? (Platform as any).isPad === true : smallestDimension >= 768);
if (isTablet) {
ScreenOrientation.unlockAsync(); ScreenOrientation.unlockAsync();
} else { } else {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
@ -567,7 +562,8 @@ const HomeScreen = () => {
return null; return null;
}, [isLoading, currentTheme.colors]); }, [isLoading, currentTheme.colors]);
const listData: HomeScreenListItem[] = useMemo(() => { // Stabilize listData to prevent FlashList re-renders
const listData = useMemo(() => {
const data: HomeScreenListItem[] = []; const data: HomeScreenListItem[] = [];
// If no addons are installed, just show the welcome component // 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) // Normal flow when addons are present (featured moved to ListHeaderComponent)
data.push({ type: 'thisWeek', key: 'thisWeek' }); data.push({ type: 'thisWeek', key: 'thisWeek' });
// Only show a limited number of catalogs initially for performance // 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 // Add a "Load More" button if there are more catalogs to show
if (catalogs.length > visibleCatalogCount && catalogs.filter(c => c).length > visibleCatalogCount) { 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; return data;
}, [hasAddons, showHeroSection, catalogs, visibleCatalogCount]); }, [hasAddons, catalogs, visibleCatalogCount]);
const handleLoadMoreCatalogs = useCallback(() => { const handleLoadMoreCatalogs = useCallback(() => {
setVisibleCatalogCount(prev => Math.min(prev + 3, catalogs.length)); setVisibleCatalogCount(prev => Math.min(prev + 3, catalogs.length));
}, [catalogs.length]); }, [catalogs.length]);
// Add memory cleanup on scroll end // Stable keyExtractor for FlashList
const handleScrollEnd = useCallback(() => { const keyExtractor = useCallback((item: HomeScreenListItem) => item.key, []);
// No-op; avoid clearing image memory cache here to prevent decode thrash/heating
// 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 // Memoize individual section components to prevent re-renders
const memoizedFeaturedContent = useMemo(() => { const memoizedFeaturedContent = useMemo(() => {
const deviceWidth = Dimensions.get('window').width;
const isTablet = deviceWidth >= 768;
const heroStyleToUse = isTablet ? 'legacy' : settings.heroStyle; const heroStyleToUse = isTablet ? 'legacy' : settings.heroStyle;
return heroStyleToUse === 'carousel' ? ( return heroStyleToUse === 'carousel' ? (
<HeroCarousel <HeroCarousel
@ -629,7 +626,7 @@ const HomeScreen = () => {
loading={featuredLoading} loading={featuredLoading}
/> />
); );
}, [settings.heroStyle, showHeroSection, featuredContentSource, featuredContent, allFeaturedContent, isSaved, handleSaveToLibrary]); }, [isTablet, settings.heroStyle, showHeroSection, featuredContentSource, featuredContent, allFeaturedContent, isSaved, handleSaveToLibrary, featuredLoading]);
const memoizedThisWeekSection = useMemo(() => <ThisWeekSection />, []); const memoizedThisWeekSection = useMemo(() => <ThisWeekSection />, []);
const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []); const memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []);
@ -649,39 +646,40 @@ const HomeScreen = () => {
HeaderVisibility.setHidden(hide); HeaderVisibility.setHidden(hide);
}, []); }, []);
const renderListItem = useCallback(({ item, index }: { item: HomeScreenListItem, index: number }) => { // Stabilize renderItem to prevent FlashList re-renders
const wrapper = (child: React.ReactNode) => ( const renderListItem = useCallback(({ item }: { item: HomeScreenListItem; index: number }) => {
<Animated.View>
{child}
</Animated.View>
);
switch (item.type) { switch (item.type) {
// featured is rendered via ListHeaderComponent to avoid remounts
case 'thisWeek': case 'thisWeek':
return wrapper(memoizedThisWeekSection); return <Animated.View>{memoizedThisWeekSection}</Animated.View>;
case 'continueWatching': case 'continueWatching':
return null; // Moved to ListHeaderComponent to avoid remounts on scroll return null; // Moved to ListHeaderComponent to avoid remounts on scroll
case 'catalog': case 'catalog':
return wrapper(<CatalogSection catalog={item.catalog} />); return (
<Animated.View>
<CatalogSection catalog={item.catalog} />
</Animated.View>
);
case 'placeholder': case 'placeholder':
return wrapper( return (
<View style={styles.catalogPlaceholder}> <Animated.View>
<View style={styles.placeholderHeader}> <View style={styles.catalogPlaceholder}>
<View style={[styles.placeholderTitle, { backgroundColor: currentTheme.colors.elevation1 }]} /> <View style={styles.placeholderHeader}>
<LoadingSpinner size="small" text="" /> <View style={[styles.placeholderTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
<LoadingSpinner size="small" text="" />
</View>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.placeholderPosters}>
{[...Array(3)].map((_, posterIndex) => (
<View
key={posterIndex}
style={[styles.placeholderPoster, { backgroundColor: currentTheme.colors.elevation1 }]}
/>
))}
</ScrollView>
</View> </View>
<ScrollView </Animated.View>
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.placeholderPosters}>
{[...Array(3)].map((_, posterIndex) => (
<View
key={posterIndex}
style={[styles.placeholderPoster, { backgroundColor: currentTheme.colors.elevation1 }]}
/>
))}
</ScrollView>
</View>
); );
case 'loadMore': case 'loadMore':
return ( return (
@ -700,19 +698,11 @@ const HomeScreen = () => {
</Animated.View> </Animated.View>
); );
case 'welcome': case 'welcome':
return wrapper(<FirstTimeWelcome />); return <Animated.View><FirstTimeWelcome /></Animated.View>;
default: default:
return null; return null;
} }
}, [ }, [memoizedThisWeekSection, currentTheme.colors.elevation1, currentTheme.colors.primary, currentTheme.colors.white, handleLoadMoreCatalogs]);
showHeroSection,
featuredContentSource,
featuredContent,
isSaved,
handleSaveToLibrary,
currentTheme.colors,
handleLoadMoreCatalogs
]);
// FlashList: using minimal props per installed version // FlashList: using minimal props per installed version
@ -737,6 +727,29 @@ const HomeScreen = () => {
</> </>
), [catalogsLoading, catalogs, loadedCatalogCount, totalCatalogsRef.current, navigation, currentTheme.colors]); ), [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 // Memoize the main content section
const renderMainContent = useMemo(() => { const renderMainContent = useMemo(() => {
if (isLoading) return null; if (isLoading) return null;
@ -751,42 +764,31 @@ const HomeScreen = () => {
<FlashList <FlashList
data={listData} data={listData}
renderItem={renderListItem} renderItem={renderListItem}
keyExtractor={item => item.key} keyExtractor={keyExtractor}
contentContainerStyle={StyleSheet.flatten([ contentContainerStyle={contentContainerStyle}
styles.scrollContent,
{ paddingTop: insets.top }
])}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
ListHeaderComponent={memoizedHeader} ListHeaderComponent={memoizedHeader}
ListFooterComponent={ListFooterComponent} ListFooterComponent={ListFooterComponent}
onEndReached={handleLoadMoreCatalogs} onEndReached={handleLoadMoreCatalogs}
onEndReachedThreshold={0.6} onEndReachedThreshold={0.6}
scrollEventThrottle={32} removeClippedSubviews={true}
onScroll={event => { scrollEventThrottle={64}
const y = event.nativeEvent.contentOffset.y; onScroll={handleScroll}
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
}
}}
/> />
{/* Toasts are rendered globally at root */} {/* Toasts are rendered globally at root */}
</View> </View>
); );
}, [ }, [
isLoading, isLoading,
currentTheme.colors, currentTheme.colors.darkBackground,
listData, listData,
renderListItem, renderListItem,
ListFooterComponent keyExtractor,
contentContainerStyle,
memoizedHeader,
ListFooterComponent,
handleLoadMoreCatalogs,
handleScroll
]); ]);
return isLoading ? renderLoadingScreen : renderMainContent; return isLoading ? renderLoadingScreen : renderMainContent;

View file

@ -9,8 +9,7 @@ import {
StatusBar, StatusBar,
Alert, Alert,
Platform, Platform,
Dimensions, Dimensions
Clipboard
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
@ -70,8 +69,7 @@ const UpdateScreen: React.FC = () => {
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);
const [isInstalling, setIsInstalling] = useState(false); const [isInstalling, setIsInstalling] = useState(false);
const [lastChecked, setLastChecked] = useState<Date | null>(null); const [lastChecked, setLastChecked] = useState<Date | null>(null);
const [logs, setLogs] = useState<string[]>([]); // Logs removed
const [showLogs, setShowLogs] = useState(false);
const [lastOperation, setLastOperation] = useState<string>(''); const [lastOperation, setLastOperation] = useState<string>('');
const [updateProgress, setUpdateProgress] = useState<number>(0); const [updateProgress, setUpdateProgress] = useState<number>(0);
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'downloading' | 'installing' | 'success' | 'error'>('idle'); const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'downloading' | 'installing' | 'success' | 'error'>('idle');
@ -87,9 +85,7 @@ const UpdateScreen: React.FC = () => {
setUpdateInfo(info); setUpdateInfo(info);
setLastChecked(new Date()); setLastChecked(new Date());
// Refresh logs after operation // Logs disabled
const logs = UpdateService.getLogs();
setLogs(logs);
if (info.isAvailable) { if (info.isAvailable) {
setUpdateStatus('available'); setUpdateStatus('available');
@ -130,9 +126,7 @@ const UpdateScreen: React.FC = () => {
setUpdateStatus('installing'); setUpdateStatus('installing');
setLastOperation('Installing update...'); setLastOperation('Installing update...');
// Refresh logs after operation // Logs disabled
const logs = UpdateService.getLogs();
setLogs(logs);
if (success) { if (success) {
setUpdateStatus('success'); setUpdateStatus('success');
@ -156,8 +150,7 @@ const UpdateScreen: React.FC = () => {
const getCurrentUpdateInfo = async () => { const getCurrentUpdateInfo = async () => {
const info = await UpdateService.getCurrentUpdateInfo(); const info = await UpdateService.getCurrentUpdateInfo();
setCurrentInfo(info); setCurrentInfo(info);
const logs = UpdateService.getLogs(); // Logs disabled
setLogs(logs);
}; };
// Extract release notes from various possible manifest fields // Extract release notes from various possible manifest fields
@ -184,41 +177,12 @@ const UpdateScreen: React.FC = () => {
); );
}; };
const refreshLogs = () => { // Logs disabled: remove actions
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');
};
const testConnectivity = async () => { const testConnectivity = async () => {
try { try {
setLastOperation('Testing connectivity...'); setLastOperation('Testing connectivity...');
const isReachable = await UpdateService.testUpdateConnectivity(); const isReachable = await UpdateService.testUpdateConnectivity();
const logs = UpdateService.getLogs();
setLogs(logs);
if (isReachable) { if (isReachable) {
setLastOperation('Update server is reachable'); setLastOperation('Update server is reachable');
@ -228,8 +192,7 @@ const UpdateScreen: React.FC = () => {
} catch (error) { } catch (error) {
if (__DEV__) console.error('Error testing connectivity:', error); if (__DEV__) console.error('Error testing connectivity:', error);
setLastOperation(`Connectivity test error: ${error instanceof Error ? error.message : 'Unknown error'}`); setLastOperation(`Connectivity test error: ${error instanceof Error ? error.message : 'Unknown error'}`);
const logs = UpdateService.getLogs(); // Logs disabled
setLogs(logs);
} }
}; };
@ -237,14 +200,11 @@ const UpdateScreen: React.FC = () => {
try { try {
setLastOperation('Testing asset URLs...'); setLastOperation('Testing asset URLs...');
await UpdateService.testAllAssetUrls(); await UpdateService.testAllAssetUrls();
const logs = UpdateService.getLogs();
setLogs(logs);
setLastOperation('Asset URL testing completed'); setLastOperation('Asset URL testing completed');
} catch (error) { } catch (error) {
if (__DEV__) console.error('Error testing asset URLs:', error); if (__DEV__) console.error('Error testing asset URLs:', error);
setLastOperation(`Asset URL test error: ${error instanceof Error ? error.message : 'Unknown error'}`); setLastOperation(`Asset URL test error: ${error instanceof Error ? error.message : 'Unknown error'}`);
const logs = UpdateService.getLogs(); // Logs disabled
setLogs(logs);
} }
}; };
@ -252,8 +212,6 @@ const UpdateScreen: React.FC = () => {
useEffect(() => { useEffect(() => {
const loadInitialData = async () => { const loadInitialData = async () => {
await getCurrentUpdateInfo(); await getCurrentUpdateInfo();
// Also refresh logs to ensure we have the latest
refreshLogs();
}; };
loadInitialData(); loadInitialData();
}, []); }, []);
@ -505,30 +463,10 @@ const UpdateScreen: React.FC = () => {
)} )}
</View> </View>
{/* Advanced Toggle */} {/* Developer Logs removed */}
<TouchableOpacity
style={[styles.modernAdvancedToggle, { backgroundColor: `${currentTheme.colors.primary}08` }]}
onPress={() => setShowLogs(!showLogs)}
activeOpacity={0.7}
>
<View style={styles.advancedToggleLeft}>
<MaterialIcons name="code" size={18} color={currentTheme.colors.primary} />
<Text style={[styles.advancedToggleLabel, { color: currentTheme.colors.primary }]}>
Developer Logs
</Text>
<View style={[styles.logsBadge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.logsBadgeText}>{logs.length}</Text>
</View>
</View>
<MaterialIcons
name={showLogs ? "keyboard-arrow-up" : "keyboard-arrow-down"}
size={20}
color={currentTheme.colors.primary}
/>
</TouchableOpacity>
</SettingsCard> </SettingsCard>
{showLogs && ( {false && (
<SettingsCard title="UPDATE LOGS" isTablet={isTablet}> <SettingsCard title="UPDATE LOGS" isTablet={isTablet}>
<View style={styles.logsContainer}> <View style={styles.logsContainer}>
<View style={styles.logsHeader}> <View style={styles.logsHeader}>
@ -550,34 +488,10 @@ const UpdateScreen: React.FC = () => {
> >
<MaterialIcons name="link" size={16} color={currentTheme.colors.primary} /> <MaterialIcons name="link" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity {/* Test log removed */}
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]} {/* Copy all logs removed */}
onPress={addTestLog} {/* Refresh logs removed */}
activeOpacity={0.7} {/* Clear logs removed */}
>
<MaterialIcons name="add" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={copyAllLogs}
activeOpacity={0.7}
>
<MaterialIcons name="content-copy" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={refreshLogs}
activeOpacity={0.7}
>
<MaterialIcons name="refresh" size={16} color={currentTheme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={clearLogs}
activeOpacity={0.7}
>
<MaterialIcons name="clear" size={16} color={currentTheme.colors.error || '#ff4444'} />
</TouchableOpacity>
</View> </View>
</View> </View>
@ -586,14 +500,12 @@ const UpdateScreen: React.FC = () => {
showsVerticalScrollIndicator={true} showsVerticalScrollIndicator={true}
nestedScrollEnabled={true} nestedScrollEnabled={true}
> >
{logs.length === 0 ? ( {false ? (
<Text style={[styles.noLogsText, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.noLogsText, { color: currentTheme.colors.mediumEmphasis }]}>No logs available</Text>
No logs available
</Text>
) : ( ) : (
logs.map((log, index) => { ([] as string[]).map((log, index) => {
const isError = log.includes('[ERROR]'); const isError = log.indexOf('[ERROR]') !== -1;
const isWarning = log.includes('[WARN]'); const isWarning = log.indexOf('[WARN]') !== -1;
return ( return (
<TouchableOpacity <TouchableOpacity
@ -602,7 +514,7 @@ const UpdateScreen: React.FC = () => {
styles.logEntry, styles.logEntry,
{ backgroundColor: 'rgba(255,255,255,0.05)' } { backgroundColor: 'rgba(255,255,255,0.05)' }
]} ]}
onPress={() => copyLog(log)} onPress={() => {}}
activeOpacity={0.7} activeOpacity={0.7}
> >
<View style={styles.logEntryContent}> <View style={styles.logEntryContent}>

View file

@ -28,61 +28,32 @@ export class UpdateService {
* Add a log entry with timestamp - always log to console for adb logcat visibility * Add a log entry with timestamp - always log to console for adb logcat visibility
*/ */
private addLog(message: string, level: 'INFO' | 'WARN' | 'ERROR' = 'INFO'): void { private addLog(message: string, level: 'INFO' | 'WARN' | 'ERROR' = 'INFO'): void {
const timestamp = new Date().toISOString(); // Logging disabled intentionally
const logEntry = `[${timestamp}] [${level}] ${message}`; return;
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}`);
}
} }
/** /**
* Get all logs * Get all logs
*/ */
public getLogs(): string[] { public getLogs(): string[] {
return [...this.logs]; // Logging disabled - return empty list
return [];
} }
/** /**
* Clear all logs * Clear all logs
*/ */
public clearLogs(): void { public clearLogs(): void {
// Logging disabled - no-op
this.logs = []; this.logs = [];
this.addLog('Logs cleared', 'INFO');
} }
/** /**
* Add a test log entry (useful for debugging) * Add a test log entry (useful for debugging)
*/ */
public addTestLog(message: string): void { public addTestLog(message: string): void {
this.addLog(`TEST: ${message}`, 'INFO'); // Logging disabled - no-op
return;
} }
/** /**