mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
potential heat fix
This commit is contained in:
parent
097073fcd3
commit
bc2a15f81f
12 changed files with 386 additions and 405 deletions
8
App.tsx
8
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<boolean | null>(null);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<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_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"/>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ interface UpdatePopupProps {
|
|||
updateInfo: {
|
||||
isAvailable: boolean;
|
||||
manifest?: {
|
||||
id: string;
|
||||
id?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<NavigationProp<RootStackParamList>>();
|
||||
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) => {
|
||||
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 (
|
||||
<ContentItem
|
||||
item={item}
|
||||
onPress={handleContentPress}
|
||||
shouldLoadImage={eager}
|
||||
deferMs={eager ? 0 : Math.min(400 + index * 15, 1500)}
|
||||
shouldLoadImage={isVisible}
|
||||
deferMs={0}
|
||||
/>
|
||||
);
|
||||
}, [handleContentPress]);
|
||||
}, [handleContentPress, visibleIndexSet]);
|
||||
|
||||
// Memoize the ItemSeparatorComponent to prevent re-creation
|
||||
const ItemSeparator = useCallback(() => <View style={{ width: 8 }} />, []);
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<ExpoImage
|
||||
source={{ uri: getOptimizedPosterUrl(item.poster) }}
|
||||
source={{ uri: optimizedPosterUrl }}
|
||||
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]}
|
||||
contentFit="cover"
|
||||
cachePolicy={Platform.OS === 'android' ? 'disk' : 'memory-disk'}
|
||||
|
|
@ -303,7 +304,9 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
export default React.memo(ContentItem, (prev, next) => {
|
||||
// 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;
|
||||
});
|
||||
|
|
@ -96,8 +96,8 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
|
||||
const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(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<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
|
||||
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<ContinueWatchingRef>((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<ContinueWatchingRef>((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<ContinueWatchingRef>((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<ContinueWatchingRef>((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<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 (continueWatchingItems.length === 0) {
|
||||
return null;
|
||||
|
|
@ -519,119 +632,15 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
|
||||
<FlashList
|
||||
data={continueWatchingItems}
|
||||
renderItem={({ item }) => (
|
||||
<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>
|
||||
)}
|
||||
keyExtractor={(item) => `continue-${item.id}-${item.type}`}
|
||||
renderItem={renderContinueWatchingItem}
|
||||
keyExtractor={keyExtractor}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.wideList}
|
||||
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
|
||||
ItemSeparatorComponent={ItemSeparator}
|
||||
onEndReachedThreshold={0.7}
|
||||
onEndReached={() => {}}
|
||||
estimatedItemSize={280 + 16}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
</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;
|
||||
});
|
||||
|
|
@ -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<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
|
@ -105,29 +106,42 @@ const NoFeaturedContent = () => {
|
|||
return (
|
||||
<View style={styles.noContentContainer}>
|
||||
<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}>
|
||||
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>
|
||||
<View style={styles.noContentButtons}>
|
||||
<TouchableOpacity
|
||||
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
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>
|
||||
{onRetry ? (
|
||||
<TouchableOpacity
|
||||
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={onRetry}
|
||||
>
|
||||
<Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Retry</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading }: FeaturedContentProps) => {
|
||||
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading, onRetry }: FeaturedContentProps) => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const [bannerUrl, setBannerUrl] = useState<string | null>(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 <NoFeaturedContent />;
|
||||
return <NoFeaturedContent onRetry={onRetry} />;
|
||||
}
|
||||
|
||||
if (isTablet) {
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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<any>}> = ({ 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());
|
||||
|
|
|
|||
|
|
@ -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<void>[] = [];
|
||||
let catalogIndex = 0;
|
||||
const catalogQueue: (() => Promise<void>)[] = [];
|
||||
|
||||
// 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<void>)[] = [];
|
||||
|
||||
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' ? (
|
||||
<HeroCarousel
|
||||
|
|
@ -629,7 +626,7 @@ const HomeScreen = () => {
|
|||
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 memoizedContinueWatchingSection = useMemo(() => <ContinueWatchingSection ref={continueWatchingRef} />, []);
|
||||
|
|
@ -649,39 +646,40 @@ const HomeScreen = () => {
|
|||
HeaderVisibility.setHidden(hide);
|
||||
}, []);
|
||||
|
||||
const renderListItem = useCallback(({ item, index }: { item: HomeScreenListItem, index: number }) => {
|
||||
const wrapper = (child: React.ReactNode) => (
|
||||
<Animated.View>
|
||||
{child}
|
||||
</Animated.View>
|
||||
);
|
||||
// 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 <Animated.View>{memoizedThisWeekSection}</Animated.View>;
|
||||
case 'continueWatching':
|
||||
return null; // Moved to ListHeaderComponent to avoid remounts on scroll
|
||||
case 'catalog':
|
||||
return wrapper(<CatalogSection catalog={item.catalog} />);
|
||||
return (
|
||||
<Animated.View>
|
||||
<CatalogSection catalog={item.catalog} />
|
||||
</Animated.View>
|
||||
);
|
||||
case 'placeholder':
|
||||
return wrapper(
|
||||
<View style={styles.catalogPlaceholder}>
|
||||
<View style={styles.placeholderHeader}>
|
||||
<View style={[styles.placeholderTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
|
||||
<LoadingSpinner size="small" text="" />
|
||||
return (
|
||||
<Animated.View>
|
||||
<View style={styles.catalogPlaceholder}>
|
||||
<View style={styles.placeholderHeader}>
|
||||
<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>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.placeholderPosters}>
|
||||
{[...Array(3)].map((_, posterIndex) => (
|
||||
<View
|
||||
key={posterIndex}
|
||||
style={[styles.placeholderPoster, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
case 'loadMore':
|
||||
return (
|
||||
|
|
@ -700,19 +698,11 @@ const HomeScreen = () => {
|
|||
</Animated.View>
|
||||
);
|
||||
case 'welcome':
|
||||
return wrapper(<FirstTimeWelcome />);
|
||||
return <Animated.View><FirstTimeWelcome /></Animated.View>;
|
||||
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 = () => {
|
|||
<FlashList
|
||||
data={listData}
|
||||
renderItem={renderListItem}
|
||||
keyExtractor={item => 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 */}
|
||||
</View>
|
||||
);
|
||||
}, [
|
||||
isLoading,
|
||||
currentTheme.colors,
|
||||
currentTheme.colors.darkBackground,
|
||||
listData,
|
||||
renderListItem,
|
||||
ListFooterComponent
|
||||
keyExtractor,
|
||||
contentContainerStyle,
|
||||
memoizedHeader,
|
||||
ListFooterComponent,
|
||||
handleLoadMoreCatalogs,
|
||||
handleScroll
|
||||
]);
|
||||
|
||||
return isLoading ? renderLoadingScreen : renderMainContent;
|
||||
|
|
|
|||
|
|
@ -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<Date | null>(null);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [showLogs, setShowLogs] = useState(false);
|
||||
// Logs removed
|
||||
const [lastOperation, setLastOperation] = useState<string>('');
|
||||
const [updateProgress, setUpdateProgress] = useState<number>(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 = () => {
|
|||
)}
|
||||
</View>
|
||||
|
||||
{/* Advanced Toggle */}
|
||||
<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>
|
||||
{/* Developer Logs removed */}
|
||||
</SettingsCard>
|
||||
|
||||
{showLogs && (
|
||||
{false && (
|
||||
<SettingsCard title="UPDATE LOGS" isTablet={isTablet}>
|
||||
<View style={styles.logsContainer}>
|
||||
<View style={styles.logsHeader}>
|
||||
|
|
@ -550,34 +488,10 @@ const UpdateScreen: React.FC = () => {
|
|||
>
|
||||
<MaterialIcons name="link" size={16} color={currentTheme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.logActionButton, { backgroundColor: currentTheme.colors.elevation2 }]}
|
||||
onPress={addTestLog}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<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>
|
||||
{/* Test log removed */}
|
||||
{/* Copy all logs removed */}
|
||||
{/* Refresh logs removed */}
|
||||
{/* Clear logs removed */}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
|
@ -586,14 +500,12 @@ const UpdateScreen: React.FC = () => {
|
|||
showsVerticalScrollIndicator={true}
|
||||
nestedScrollEnabled={true}
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<Text style={[styles.noLogsText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
No logs available
|
||||
</Text>
|
||||
{false ? (
|
||||
<Text style={[styles.noLogsText, { color: currentTheme.colors.mediumEmphasis }]}>No logs available</Text>
|
||||
) : (
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
|
|
@ -602,7 +514,7 @@ const UpdateScreen: React.FC = () => {
|
|||
styles.logEntry,
|
||||
{ backgroundColor: 'rgba(255,255,255,0.05)' }
|
||||
]}
|
||||
onPress={() => copyLog(log)}
|
||||
onPress={() => {}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.logEntryContent}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue