Merge branch 'tapframe:main' into customConfirmation
This commit is contained in:
commit
5d2fdbdde1
5 changed files with 136 additions and 94 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||
import { View, TouchableOpacity, TouchableWithoutFeedback, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet, Modal, AppState } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video';
|
||||
import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType, ViewType } from 'react-native-video';
|
||||
import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import { PinchGestureHandler, PanGestureHandler, TapGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler';
|
||||
|
|
@ -1135,27 +1135,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
};
|
||||
});
|
||||
setRnVideoAudioTracks(formattedAudioTracks);
|
||||
|
||||
// Auto-select audio track if none is selected (similar to iOS behavior)
|
||||
if (selectedAudioTrack?.type === SelectedTrackType.SYSTEM && formattedAudioTracks.length > 0) {
|
||||
// Look for English track first
|
||||
const englishTrack = formattedAudioTracks.find((track: {id: number, name: string, language?: string}) => {
|
||||
const lang = (track.language || '').toLowerCase();
|
||||
return lang === 'english' || lang === 'en' || lang === 'eng' ||
|
||||
(track.name && track.name.toLowerCase().includes('english'));
|
||||
});
|
||||
|
||||
const selectedTrack = englishTrack || formattedAudioTracks[0];
|
||||
setSelectedAudioTrack({ type: SelectedTrackType.INDEX, value: selectedTrack.id });
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
if (englishTrack) {
|
||||
logger.log(`[AndroidVideoPlayer] Auto-selected English audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
|
||||
} else {
|
||||
logger.log(`[AndroidVideoPlayer] No English track found, auto-selected first audio track: ${selectedTrack.name} (ID: ${selectedTrack.id})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[AndroidVideoPlayer] Formatted audio tracks:`, formattedAudioTracks);
|
||||
|
|
@ -2770,8 +2749,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
allowsExternalPlayback={false as any}
|
||||
preventsDisplaySleepDuringVideoPlayback={true as any}
|
||||
// ExoPlayer HLS optimization - let the player use optimal defaults
|
||||
// Use SurfaceView on Android to lower memory pressure with 4K/high-bitrate content
|
||||
useTextureView={Platform.OS === 'android' ? false : (undefined as any)}
|
||||
// Use textureView on Android: allows 3D mapping but DRM not supported
|
||||
viewType={Platform.OS === 'android' ? ViewType.TEXTURE : undefined}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
RefreshControl,
|
||||
Dimensions,
|
||||
Platform,
|
||||
InteractionManager
|
||||
} from 'react-native';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
|
|
@ -226,6 +227,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
const [dataSource, setDataSource] = useState<DataSource>(DataSource.STREMIO_ADDONS);
|
||||
const [actualCatalogName, setActualCatalogName] = useState<string | null>(null);
|
||||
const [screenData, setScreenData] = useState(() => {
|
||||
|
|
@ -403,24 +405,30 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
index === self.findIndex((t) => t.id === item.id)
|
||||
);
|
||||
|
||||
setItems(uniqueItems);
|
||||
setHasMore(false); // TMDB already returns a full set
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setItems(uniqueItems);
|
||||
setHasMore(false); // TMDB already returns a full set
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
setError("No content found for the selected filters");
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setError("No content found for the selected filters");
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to get TMDB catalog:', error);
|
||||
setError('Failed to load content from TMDB');
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setError('Failed to load content from TMDB');
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -448,7 +456,9 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
|
||||
if (catalogItems.length > 0) {
|
||||
foundItems = true;
|
||||
setItems(catalogItems);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setItems(catalogItems);
|
||||
});
|
||||
}
|
||||
} else if (effectiveGenreFilter) {
|
||||
// Get all addons that have catalogs of the specified type
|
||||
|
|
@ -527,19 +537,27 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
|
||||
if (uniqueItems.length > 0) {
|
||||
foundItems = true;
|
||||
setItems(uniqueItems);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setItems(uniqueItems);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundItems) {
|
||||
setError("No content found for the selected filters");
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setError("No content found for the selected filters");
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load catalog items');
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load catalog items');
|
||||
});
|
||||
logger.error('Failed to load catalog:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
});
|
||||
}
|
||||
}, [addonId, type, id, genreFilter, dataSource]);
|
||||
|
||||
|
|
@ -651,7 +669,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={() => loadItems(1)}
|
||||
onPress={() => loadItems(true)}
|
||||
>
|
||||
<Text style={styles.buttonText}>Retry</Text>
|
||||
</TouchableOpacity>
|
||||
|
|
@ -736,7 +754,6 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
}
|
||||
contentContainerStyle={styles.list}
|
||||
showsVerticalScrollIndicator={false}
|
||||
estimatedItemSize={effectiveItemWidth * 1.5 + SPACING.lg}
|
||||
/>
|
||||
) : renderEmptyState()}
|
||||
</SafeAreaView>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ import {
|
|||
Image,
|
||||
Modal,
|
||||
Pressable,
|
||||
Alert
|
||||
Alert,
|
||||
InteractionManager
|
||||
} from 'react-native';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
|
|
@ -146,8 +147,10 @@ const HomeScreen = () => {
|
|||
stremioService.getInstalledAddonsAsync()
|
||||
]);
|
||||
|
||||
// Set hasAddons state based on whether we have any addons
|
||||
setHasAddons(addons.length > 0);
|
||||
// Set hasAddons state based on whether we have any addons - ensure on main thread
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setHasAddons(addons.length > 0);
|
||||
});
|
||||
|
||||
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
||||
|
||||
|
|
@ -245,23 +248,28 @@ const HomeScreen = () => {
|
|||
items
|
||||
};
|
||||
|
||||
// Update the catalog at its specific position
|
||||
setCatalogs(prevCatalogs => {
|
||||
const newCatalogs = [...prevCatalogs];
|
||||
newCatalogs[currentIndex] = catalogContent;
|
||||
return newCatalogs;
|
||||
// Update the catalog at its specific position - ensure on main thread
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setCatalogs(prevCatalogs => {
|
||||
const newCatalogs = [...prevCatalogs];
|
||||
newCatalogs[currentIndex] = catalogContent;
|
||||
return newCatalogs;
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error(`[HomeScreen] Failed to load ${catalog.name} from ${addon.name}:`, error);
|
||||
} finally {
|
||||
setLoadedCatalogCount(prev => {
|
||||
const next = prev + 1;
|
||||
// Exit loading screen as soon as first catalog finishes
|
||||
if (prev === 0) {
|
||||
setCatalogsLoading(false);
|
||||
}
|
||||
return next;
|
||||
// Update loading count - ensure on main thread
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setLoadedCatalogCount(prev => {
|
||||
const next = prev + 1;
|
||||
// Exit loading screen as soon as first catalog finishes
|
||||
if (prev === 0) {
|
||||
setCatalogsLoading(false);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -275,14 +283,18 @@ const HomeScreen = () => {
|
|||
|
||||
totalCatalogsRef.current = catalogIndex;
|
||||
|
||||
// Initialize catalogs array with proper length
|
||||
setCatalogs(new Array(catalogIndex).fill(null));
|
||||
// Initialize catalogs array with proper length - ensure on main thread
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setCatalogs(new Array(catalogIndex).fill(null));
|
||||
});
|
||||
|
||||
// Start processing the catalog queue
|
||||
processCatalogQueue();
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('[HomeScreen] Error in progressive catalog loading:', error);
|
||||
setCatalogsLoading(false);
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setCatalogsLoading(false);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -80,29 +80,38 @@ const detectMkvViaHead = async (url: string, headers?: Record<string, string>) =
|
|||
};
|
||||
|
||||
// Animated Components
|
||||
const AnimatedImage = memo(({
|
||||
source,
|
||||
style,
|
||||
contentFit,
|
||||
onLoad
|
||||
}: {
|
||||
source: { uri: string } | undefined;
|
||||
style: any;
|
||||
contentFit: any;
|
||||
const AnimatedImage = memo(({
|
||||
source,
|
||||
style,
|
||||
contentFit,
|
||||
onLoad
|
||||
}: {
|
||||
source: { uri: string } | undefined;
|
||||
style: any;
|
||||
contentFit: any;
|
||||
onLoad?: () => void;
|
||||
}) => {
|
||||
const opacity = useSharedValue(0);
|
||||
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
}));
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (source?.uri) {
|
||||
opacity.value = withTiming(1, { duration: 300 });
|
||||
} else {
|
||||
opacity.value = 0;
|
||||
}
|
||||
}, [source?.uri]);
|
||||
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
opacity.value = 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Animated.View style={[style, animatedStyle]}>
|
||||
<Image
|
||||
|
|
@ -115,30 +124,38 @@ const AnimatedImage = memo(({
|
|||
);
|
||||
});
|
||||
|
||||
const AnimatedText = memo(({
|
||||
children,
|
||||
style,
|
||||
const AnimatedText = memo(({
|
||||
children,
|
||||
style,
|
||||
delay = 0,
|
||||
numberOfLines
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
style: any;
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
style: any;
|
||||
delay?: number;
|
||||
numberOfLines?: number;
|
||||
}) => {
|
||||
const opacity = useSharedValue(0);
|
||||
const translateY = useSharedValue(20);
|
||||
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
transform: [{ translateY: translateY.value }],
|
||||
}));
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = withDelay(delay, withTiming(1, { duration: 250 }));
|
||||
translateY.value = withDelay(delay, withTiming(0, { duration: 250 }));
|
||||
}, [delay]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
opacity.value = 0;
|
||||
translateY.value = 20;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Animated.Text style={[style, animatedStyle]} numberOfLines={numberOfLines}>
|
||||
{children}
|
||||
|
|
@ -146,28 +163,36 @@ const AnimatedText = memo(({
|
|||
);
|
||||
});
|
||||
|
||||
const AnimatedView = memo(({
|
||||
children,
|
||||
style,
|
||||
delay = 0
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
style?: any;
|
||||
const AnimatedView = memo(({
|
||||
children,
|
||||
style,
|
||||
delay = 0
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
style?: any;
|
||||
delay?: number;
|
||||
}) => {
|
||||
const opacity = useSharedValue(0);
|
||||
const translateY = useSharedValue(20);
|
||||
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
transform: [{ translateY: translateY.value }],
|
||||
}));
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = withDelay(delay, withTiming(1, { duration: 250 }));
|
||||
translateY.value = withDelay(delay, withTiming(0, { duration: 250 }));
|
||||
}, [delay]);
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
opacity.value = 0;
|
||||
translateY.value = 20;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Animated.View style={[style, animatedStyle]}>
|
||||
{children}
|
||||
|
|
@ -381,6 +406,7 @@ const ProviderFilter = memo(({
|
|||
initialNumToRender={5}
|
||||
maxToRenderPerBatch={3}
|
||||
windowSize={3}
|
||||
removeClippedSubviews={true}
|
||||
getItemLayout={(data, index) => ({
|
||||
length: 100, // Approximate width of each item
|
||||
offset: 100 * index,
|
||||
|
|
@ -1608,6 +1634,9 @@ export const StreamsScreen = () => {
|
|||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
// Clear scraper logo cache to free memory
|
||||
scraperLogoCache.clear();
|
||||
scraperLogoCachePromise = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
@ -1867,6 +1896,11 @@ export const StreamsScreen = () => {
|
|||
windowSize={3}
|
||||
removeClippedSubviews={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
getItemLayout={(data, index) => ({
|
||||
length: 78, // Approximate height of StreamCard (68 minHeight + 10 marginBottom)
|
||||
offset: 78 * index,
|
||||
index,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
// Empty section placeholder
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ Answer questions about this movie using only the verified database information a
|
|||
'X-Title': 'Nuvio - AI Chat',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'openrouter/sonoma-dusk-alpha',
|
||||
model: 'x-ai/grok-4-fast:free',
|
||||
messages,
|
||||
max_tokens: 1000,
|
||||
temperature: 0.7,
|
||||
|
|
|
|||
Loading…
Reference in a new issue