fixed audio selection issue

This commit is contained in:
tapframe 2025-09-12 22:48:26 +05:30
parent fa7352f4fa
commit fa03d4455f
6 changed files with 344 additions and 143 deletions

1
KSPlayer Submodule

@ -0,0 +1 @@
Subproject commit 8fe5feb73ca3ee5092d2ed1dd8fcb692c10e3c11

@ -1 +1 @@
Subproject commit 5c020cca433f0400e23eb553f3e4de09f65b66d3 Subproject commit 0a273ba5eb2632979a6678e005314c5b3ffb70eb

View file

@ -757,12 +757,35 @@ const AndroidVideoPlayer: React.FC = () => {
// Handle audio tracks // Handle audio tracks
if (data.audioTracks && data.audioTracks.length > 0) { if (data.audioTracks && data.audioTracks.length > 0) {
const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => ({ const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => {
id: track.index || index, const trackIndex = track.index !== undefined ? track.index : index;
name: track.title || track.language || `Audio ${index + 1}`, const trackName = track.title || track.language || `Audio ${index + 1}`;
language: track.language, const trackLanguage = track.language || 'Unknown';
}));
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Audio track ${index}: index=${trackIndex}, name="${trackName}", language="${trackLanguage}"`);
}
return {
id: trackIndex, // Use the actual track index from react-native-video
name: trackName,
language: trackLanguage,
};
});
setRnVideoAudioTracks(formattedAudioTracks); setRnVideoAudioTracks(formattedAudioTracks);
// Auto-select the first audio track if none is selected
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
const firstTrack = formattedAudioTracks[0];
setSelectedAudioTrack(firstTrack.id);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Auto-selected first audio track: ${firstTrack.name} (ID: ${firstTrack.id})`);
}
}
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Formatted audio tracks:`, formattedAudioTracks);
}
} }
// Handle text tracks // Handle text tracks
@ -1205,7 +1228,42 @@ const AndroidVideoPlayer: React.FC = () => {
}; };
const selectAudioTrack = (trackId: number) => { const selectAudioTrack = (trackId: number) => {
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Selecting audio track: ${trackId}`);
logger.log(`[AndroidVideoPlayer] Available tracks:`, rnVideoAudioTracks);
}
// Validate that the track exists
const trackExists = rnVideoAudioTracks.some(track => track.id === trackId);
if (!trackExists) {
logger.error(`[AndroidVideoPlayer] Audio track ${trackId} not found in available tracks`);
return;
}
// If changing tracks, briefly pause to allow smooth transition
const wasPlaying = !paused;
if (wasPlaying) {
setPaused(true);
}
// Set the new audio track
setSelectedAudioTrack(trackId); setSelectedAudioTrack(trackId);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Audio track changed to: ${trackId}`);
}
// Resume playback after a brief delay if it was playing
if (wasPlaying) {
setTimeout(() => {
if (isMounted.current) {
setPaused(false);
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Resumed playback after audio track change`);
}
}
}, 300);
}
}; };
const selectTextTrack = (trackId: number) => { const selectTextTrack = (trackId: number) => {
@ -1737,6 +1795,20 @@ const AndroidVideoPlayer: React.FC = () => {
loadSubtitleSize(); loadSubtitleSize();
}, []); }, []);
// Handle audio track changes with proper logging
useEffect(() => {
if (selectedAudioTrack !== null && rnVideoAudioTracks.length > 0) {
const selectedTrack = rnVideoAudioTracks.find(track => track.id === selectedAudioTrack);
if (selectedTrack) {
if (DEBUG_MODE) {
logger.log(`[AndroidVideoPlayer] Audio track selected: ${selectedTrack.name} (${selectedTrack.language}) - ID: ${selectedAudioTrack}`);
}
} else {
logger.warn(`[AndroidVideoPlayer] Selected audio track ${selectedAudioTrack} not found in available tracks`);
}
}
}, [selectedAudioTrack, rnVideoAudioTracks]);
// Load global subtitle settings // Load global subtitle settings
useEffect(() => { useEffect(() => {
(async () => { (async () => {

View file

@ -751,7 +751,35 @@ const VideoPlayer: React.FC = () => {
} }
if (data.audioTracks && data.audioTracks.length > 0) { if (data.audioTracks && data.audioTracks.length > 0) {
setVlcAudioTracks(data.audioTracks); const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => {
const trackIndex = track.index !== undefined ? track.index : index;
const trackName = track.title || track.language || `Audio ${index + 1}`;
const trackLanguage = track.language || 'Unknown';
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Audio track ${index}: index=${trackIndex}, name="${trackName}", language="${trackLanguage}"`);
}
return {
id: trackIndex, // Use the actual track index from VLC
name: trackName,
language: trackLanguage,
};
});
setVlcAudioTracks(formattedAudioTracks);
// Auto-select the first audio track if none is selected
if (selectedAudioTrack === null && formattedAudioTracks.length > 0) {
const firstTrack = formattedAudioTracks[0];
setSelectedAudioTrack(firstTrack.id);
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Auto-selected first audio track: ${firstTrack.name} (ID: ${firstTrack.id})`);
}
}
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Formatted audio tracks:`, formattedAudioTracks);
}
} }
if (data.textTracks && data.textTracks.length > 0) { if (data.textTracks && data.textTracks.length > 0) {
setVlcTextTracks(data.textTracks); setVlcTextTracks(data.textTracks);
@ -1024,7 +1052,42 @@ const VideoPlayer: React.FC = () => {
}; };
const selectAudioTrack = (trackId: number) => { const selectAudioTrack = (trackId: number) => {
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Selecting audio track: ${trackId}`);
logger.log(`[VideoPlayer] Available tracks:`, vlcAudioTracks);
}
// Validate that the track exists
const trackExists = vlcAudioTracks.some(track => track.id === trackId);
if (!trackExists) {
logger.error(`[VideoPlayer] Audio track ${trackId} not found in available tracks`);
return;
}
// If changing tracks, briefly pause to allow smooth transition
const wasPlaying = !paused;
if (wasPlaying) {
setPaused(true);
}
// Set the new audio track
setSelectedAudioTrack(trackId); setSelectedAudioTrack(trackId);
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Audio track changed to: ${trackId}`);
}
// Resume playback after a brief delay if it was playing
if (wasPlaying) {
setTimeout(() => {
if (isMounted.current) {
setPaused(false);
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Resumed playback after audio track change`);
}
}
}, 300);
}
}; };
const selectTextTrack = (trackId: number) => { const selectTextTrack = (trackId: number) => {
@ -1578,6 +1641,20 @@ const VideoPlayer: React.FC = () => {
loadSubtitleSize(); loadSubtitleSize();
}, []); }, []);
// Handle audio track changes with proper logging
useEffect(() => {
if (selectedAudioTrack !== null && vlcAudioTracks.length > 0) {
const selectedTrack = vlcAudioTracks.find(track => track.id === selectedAudioTrack);
if (selectedTrack) {
if (DEBUG_MODE) {
logger.log(`[VideoPlayer] Audio track selected: ${selectedTrack.name} (${selectedTrack.language}) - ID: ${selectedAudioTrack}`);
}
} else {
logger.warn(`[VideoPlayer] Selected audio track ${selectedAudioTrack} not found in available tracks`);
}
}
}, [selectedAudioTrack, vlcAudioTracks]);
const increaseSubtitleSize = () => { const increaseSubtitleSize = () => {
const newSize = Math.min(subtitleSize + 2, 32); const newSize = Math.min(subtitleSize + 2, 32);
saveSubtitleSize(newSize); saveSubtitleSize(newSize);

View file

@ -7,7 +7,8 @@ import Animated, {
SlideInRight, SlideInRight,
SlideOutRight, SlideOutRight,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { getTrackDisplayName } from '../utils/playerUtils'; import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
import { logger } from '../../../utils/logger';
interface AudioTrackModalProps { interface AudioTrackModalProps {
showAudioModal: boolean; showAudioModal: boolean;
@ -31,6 +32,20 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
setShowAudioModal(false); setShowAudioModal(false);
}; };
// Debug logging when modal opens
React.useEffect(() => {
if (showAudioModal && DEBUG_MODE) {
logger.log(`[AudioTrackModal] Modal opened with selectedAudioTrack: ${selectedAudioTrack}`);
logger.log(`[AudioTrackModal] Available tracks:`, vlcAudioTracks);
const selectedTrack = vlcAudioTracks.find(track => track.id === selectedAudioTrack);
if (selectedTrack) {
logger.log(`[AudioTrackModal] Selected track found: ${selectedTrack.name} (${selectedTrack.language})`);
} else {
logger.warn(`[AudioTrackModal] Selected track ${selectedAudioTrack} not found in available tracks`);
}
}
}, [showAudioModal, selectedAudioTrack, vlcAudioTracks]);
if (!showAudioModal) return null; if (!showAudioModal) return null;
return ( return (
@ -131,7 +146,9 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
<View style={{ gap: 8 }}> <View style={{ gap: 8 }}>
{vlcAudioTracks.map((track) => { {vlcAudioTracks.map((track) => {
const isSelected = selectedAudioTrack === track.id; // If no track is selected, show the first track as selected
const isSelected = selectedAudioTrack === track.id ||
(selectedAudioTrack === null && track.id === vlcAudioTracks[0]?.id);
return ( return (
<TouchableOpacity <TouchableOpacity
key={track.id} key={track.id}
@ -143,7 +160,14 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)', borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
}} }}
onPress={() => { onPress={() => {
if (DEBUG_MODE) {
logger.log(`[AudioTrackModal] Selecting track: ${track.id} (${track.name})`);
}
selectAudioTrack(track.id); selectAudioTrack(track.id);
// Close modal after selection
setTimeout(() => {
setShowAudioModal(false);
}, 200);
}} }}
activeOpacity={0.7} activeOpacity={0.7}
> >

View file

@ -17,6 +17,13 @@ import {
Clipboard, Clipboard,
Image as RNImage, Image as RNImage,
} from 'react-native'; } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withDelay,
runOnJS
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as ScreenOrientation from 'expo-screen-orientation'; import * as ScreenOrientation from 'expo-screen-orientation';
@ -38,22 +45,6 @@ import { localScraperService } from '../services/localScraperService';
import { VideoPlayerService } from '../services/videoPlayerService'; import { VideoPlayerService } from '../services/videoPlayerService';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import QualityBadge from '../components/metadata/QualityBadge'; import QualityBadge from '../components/metadata/QualityBadge';
import Animated, {
FadeIn,
FadeOut,
FadeInDown,
SlideInDown,
withSpring,
withTiming,
useAnimatedStyle,
useSharedValue,
interpolate,
Extrapolate,
runOnJS,
cancelAnimation,
SharedValue,
Layout
} from 'react-native-reanimated';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906'; const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906';
@ -87,6 +78,102 @@ 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;
onLoad?: () => void;
}) => {
const opacity = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
useEffect(() => {
if (source?.uri) {
opacity.value = withTiming(1, { duration: 300 });
}
}, [source?.uri]);
return (
<Animated.View style={[style, animatedStyle]}>
<Image
source={source}
style={StyleSheet.absoluteFillObject}
contentFit={contentFit}
onLoad={onLoad}
/>
</Animated.View>
);
});
const AnimatedText = memo(({
children,
style,
delay = 0,
numberOfLines
}: {
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 }));
}, []);
return (
<Animated.Text style={[style, animatedStyle]} numberOfLines={numberOfLines}>
{children}
</Animated.Text>
);
});
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 }));
}, []);
return (
<Animated.View style={[style, animatedStyle]}>
{children}
</Animated.View>
);
});
// Extracted Components // Extracted Components
const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos, scraperLogo }: { const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos, scraperLogo }: {
stream: Stream; stream: Stream;
@ -269,22 +356,20 @@ const ProviderFilter = memo(({
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => ( const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
<Animated.View entering={FadeIn.duration(300).delay(index * 75)}> <TouchableOpacity
<TouchableOpacity style={[
style={[ styles.filterChip,
styles.filterChip, selectedProvider === item.id && styles.filterChipSelected
selectedProvider === item.id && styles.filterChipSelected ]}
]} onPress={() => onSelect(item.id)}
onPress={() => onSelect(item.id)} >
> <Text style={[
<Text style={[ styles.filterChipText,
styles.filterChipText, selectedProvider === item.id && styles.filterChipTextSelected
selectedProvider === item.id && styles.filterChipTextSelected ]}>
]}> {item.name}
{item.name} </Text>
</Text> </TouchableOpacity>
</TouchableOpacity>
</Animated.View>
), [selectedProvider, onSelect, styles]); ), [selectedProvider, onSelect, styles]);
return ( return (
@ -383,10 +468,6 @@ export const StreamsScreen = () => {
const [selectedProvider, setSelectedProvider] = React.useState('all'); const [selectedProvider, setSelectedProvider] = React.useState('all');
const [availableProviders, setAvailableProviders] = React.useState<Set<string>>(new Set()); const [availableProviders, setAvailableProviders] = React.useState<Set<string>>(new Set());
// Optimize animation values with cleanup
const headerOpacity = useSharedValue(0);
const heroScale = useSharedValue(0.95);
const filterOpacity = useSharedValue(0);
// Add state for provider loading status // Add state for provider loading status
const [loadingProviders, setLoadingProviders] = useState<{[key: string]: boolean}>({}); const [loadingProviders, setLoadingProviders] = useState<{[key: string]: boolean}>({});
@ -571,34 +652,9 @@ export const StreamsScreen = () => {
checkProviders(); checkProviders();
}, [type, id, episodeId, settings.autoplayBestStream, fromPlayer]); }, [type, id, episodeId, settings.autoplayBestStream, fromPlayer]);
React.useEffect(() => {
// Trigger entrance animations
headerOpacity.value = withTiming(1, { duration: 400 });
heroScale.value = withSpring(1, {
damping: 15,
stiffness: 100,
mass: 0.9,
restDisplacementThreshold: 0.01
});
filterOpacity.value = withTiming(1, { duration: 500 });
return () => {
// Cleanup animations on unmount
cancelAnimation(headerOpacity);
cancelAnimation(heroScale);
cancelAnimation(filterOpacity);
};
}, []);
// Memoize handlers // Memoize handlers
const handleBack = useCallback(() => { const handleBack = useCallback(() => {
const cleanup = () => {
headerOpacity.value = withTiming(0, { duration: 100 });
heroScale.value = withTiming(0.95, { duration: 100 });
filterOpacity.value = withTiming(0, { duration: 100 });
};
cleanup();
if (type === 'series') { if (type === 'series') {
// Reset stack to ensure there is always a screen to go back to from Metadata // Reset stack to ensure there is always a screen to go back to from Metadata
(navigation as any).reset({ (navigation as any).reset({
@ -616,7 +672,7 @@ export const StreamsScreen = () => {
} else { } else {
(navigation as any).navigate('MainTabs'); (navigation as any).navigate('MainTabs');
} }
}, [navigation, headerOpacity, heroScale, filterOpacity, type, id]); }, [navigation, type, id]);
const handleProviderChange = useCallback((provider: string) => { const handleProviderChange = useCallback((provider: string) => {
setSelectedProvider(provider); setSelectedProvider(provider);
@ -1430,24 +1486,6 @@ export const StreamsScreen = () => {
const showInitialLoading = streamsEmpty && (streamsLoadStart === null || loadElapsed < 10000); const showInitialLoading = streamsEmpty && (streamsLoadStart === null || loadElapsed < 10000);
const showStillFetching = streamsEmpty && loadElapsed >= 10000; const showStillFetching = streamsEmpty && loadElapsed >= 10000;
const heroStyle = useAnimatedStyle(() => ({
transform: [{ scale: heroScale.value }],
opacity: headerOpacity.value
}));
const filterStyle = useAnimatedStyle(() => ({
opacity: filterOpacity.value,
transform: [
{
translateY: interpolate(
filterOpacity.value,
[0, 1],
[20, 0],
Extrapolate.CLAMP
)
}
]
}));
const renderItem = useCallback(({ item, index, section }: { item: Stream; index: number; section: any }) => { const renderItem = useCallback(({ item, index, section }: { item: Stream; index: number; section: any }) => {
const stream = item; const stream = item;
@ -1509,7 +1547,7 @@ export const StreamsScreen = () => {
{Platform.OS !== 'ios' && ( {Platform.OS !== 'ios' && (
<Animated.View <View
style={[styles.backButtonContainer]} style={[styles.backButtonContainer]}
> >
<TouchableOpacity <TouchableOpacity
@ -1525,38 +1563,37 @@ export const StreamsScreen = () => {
{type === 'series' ? 'Back to Episodes' : 'Back to Info'} {type === 'series' ? 'Back to Episodes' : 'Back to Info'}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </View>
)} )}
{type === 'movie' && metadata && ( {type === 'movie' && metadata && (
<Animated.View style={[styles.movieTitleContainer, heroStyle]}> <View style={[styles.movieTitleContainer]}>
<View style={styles.movieTitleContent}> <View style={styles.movieTitleContent}>
{metadata.logo ? ( {metadata.logo ? (
<Image <AnimatedImage
source={{ uri: metadata.logo }} source={{ uri: metadata.logo }}
style={styles.movieLogo} style={styles.movieLogo}
contentFit="contain" contentFit="contain"
/> />
) : ( ) : (
<Text style={styles.movieTitle} numberOfLines={2}> <AnimatedText style={styles.movieTitle} numberOfLines={2}>
{metadata.name} {metadata.name}
</Text> </AnimatedText>
)} )}
</View> </View>
</Animated.View> </View>
)} )}
{type === 'series' && ( {type === 'series' && (
<Animated.View style={[styles.streamsHeroContainer, heroStyle]}> <View style={[styles.streamsHeroContainer]}>
<Animated.View style={StyleSheet.absoluteFill}> <View style={StyleSheet.absoluteFill}>
<Animated.View <View
style={StyleSheet.absoluteFill} style={StyleSheet.absoluteFill}
> >
<Image <AnimatedImage
source={episodeImage ? { uri: episodeImage } : undefined} source={episodeImage ? { uri: episodeImage } : undefined}
style={styles.streamsHeroBackground} style={styles.streamsHeroBackground}
contentFit="cover" contentFit="cover"
transition={500}
/> />
<LinearGradient <LinearGradient
colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.3)', 'rgba(0,0,0,0.5)', 'rgba(0,0,0,0.7)', colors.darkBackground]} colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.3)', 'rgba(0,0,0,0.5)', 'rgba(0,0,0,0.7)', colors.darkBackground]}
@ -1565,58 +1602,60 @@ export const StreamsScreen = () => {
> >
<View style={styles.streamsHeroContent}> <View style={styles.streamsHeroContent}>
{currentEpisode ? ( {currentEpisode ? (
<Animated.View style={styles.streamsHeroInfo}> <View style={styles.streamsHeroInfo}>
<Text style={styles.streamsHeroEpisodeNumber}>{currentEpisode.episodeString}</Text> <AnimatedText style={styles.streamsHeroEpisodeNumber} delay={50}>
<Text style={styles.streamsHeroTitle} numberOfLines={1}> {currentEpisode.episodeString}
</AnimatedText>
<AnimatedText style={styles.streamsHeroTitle} numberOfLines={1} delay={100}>
{currentEpisode.name} {currentEpisode.name}
</Text> </AnimatedText>
{!!currentEpisode.overview && ( {!!currentEpisode.overview && (
<Animated.View> <AnimatedView delay={150}>
<Text style={styles.streamsHeroOverview} numberOfLines={2}> <Text style={styles.streamsHeroOverview} numberOfLines={2}>
{currentEpisode.overview} {currentEpisode.overview}
</Text> </Text>
</Animated.View> </AnimatedView>
)} )}
<Animated.View style={styles.streamsHeroMeta}> <AnimatedView style={styles.streamsHeroMeta} delay={200}>
<Text style={styles.streamsHeroReleased}> <Text style={styles.streamsHeroReleased}>
{tmdbService.formatAirDate(currentEpisode.air_date)} {tmdbService.formatAirDate(currentEpisode.air_date)}
</Text> </Text>
{effectiveEpisodeVote > 0 && ( {effectiveEpisodeVote > 0 && (
<Animated.View style={styles.streamsHeroRating}> <View style={styles.streamsHeroRating}>
<Image source={{ uri: TMDB_LOGO }} style={styles.tmdbLogo} contentFit="contain" /> <Image source={{ uri: TMDB_LOGO }} style={styles.tmdbLogo} contentFit="contain" />
<Text style={styles.streamsHeroRatingText}> <Text style={styles.streamsHeroRatingText}>
{effectiveEpisodeVote.toFixed(1)} {effectiveEpisodeVote.toFixed(1)}
</Text> </Text>
</Animated.View> </View>
)} )}
{!!effectiveEpisodeRuntime && ( {!!effectiveEpisodeRuntime && (
<Animated.View style={styles.streamsHeroRuntime}> <View style={styles.streamsHeroRuntime}>
<MaterialIcons name="schedule" size={16} color={colors.mediumEmphasis} /> <MaterialIcons name="schedule" size={16} color={colors.mediumEmphasis} />
<Text style={styles.streamsHeroRuntimeText}> <Text style={styles.streamsHeroRuntimeText}>
{effectiveEpisodeRuntime >= 60 {effectiveEpisodeRuntime >= 60
? `${Math.floor(effectiveEpisodeRuntime / 60)}h ${effectiveEpisodeRuntime % 60}m` ? `${Math.floor(effectiveEpisodeRuntime / 60)}h ${effectiveEpisodeRuntime % 60}m`
: `${effectiveEpisodeRuntime}m`} : `${effectiveEpisodeRuntime}m`}
</Text> </Text>
</Animated.View> </View>
)} )}
</Animated.View> </AnimatedView>
</Animated.View> </View>
) : ( ) : (
// Placeholder to reserve space and avoid layout shift while loading // Placeholder to reserve space and avoid layout shift while loading
<View style={{ width: '100%', height: 120 }} /> <View style={{ width: '100%', height: 120 }} />
)} )}
</View> </View>
</LinearGradient> </LinearGradient>
</Animated.View> </View>
</Animated.View> </View>
</Animated.View> </View>
)} )}
<View style={[ <View style={[
styles.streamsMainContent, styles.streamsMainContent,
type === 'movie' && styles.streamsMainContentMovie type === 'movie' && styles.streamsMainContentMovie
]}> ]}>
<Animated.View style={[styles.filterContainer, filterStyle]}> <View style={[styles.filterContainer]}>
{Object.keys(streams).length > 0 && ( {Object.keys(streams).length > 0 && (
<ProviderFilter <ProviderFilter
selectedProvider={selectedProvider} selectedProvider={selectedProvider}
@ -1625,11 +1664,11 @@ export const StreamsScreen = () => {
theme={currentTheme} theme={currentTheme}
/> />
)} )}
</Animated.View> </View>
{/* Active Scrapers Status */} {/* Active Scrapers Status */}
{activeFetchingScrapers.length > 0 && ( {activeFetchingScrapers.length > 0 && (
<Animated.View <View
style={styles.activeScrapersContainer} style={styles.activeScrapersContainer}
> >
<Text style={styles.activeScrapersTitle}>Fetching from:</Text> <Text style={styles.activeScrapersTitle}>Fetching from:</Text>
@ -1638,13 +1677,12 @@ export const StreamsScreen = () => {
<PulsingChip key={scraperName} text={scraperName} delay={index * 200} /> <PulsingChip key={scraperName} text={scraperName} delay={index * 200} />
))} ))}
</View> </View>
</Animated.View> </View>
)} )}
{/* Update the streams/loading state display logic */} {/* Update the streams/loading state display logic */}
{ showNoSourcesError ? ( { showNoSourcesError ? (
<Animated.View <View
entering={FadeIn.duration(300)}
style={styles.noStreams} style={styles.noStreams}
> >
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} /> <MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
@ -1658,46 +1696,46 @@ export const StreamsScreen = () => {
> >
<Text style={styles.addSourcesButtonText}>Add Sources</Text> <Text style={styles.addSourcesButtonText}>Add Sources</Text>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </View>
) : streamsEmpty ? ( ) : streamsEmpty ? (
showInitialLoading ? ( showInitialLoading ? (
<Animated.View <View
style={styles.loadingContainer} style={styles.loadingContainer}
> >
<ActivityIndicator size="large" color={colors.primary} /> <ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}> <Text style={styles.loadingText}>
{isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'} {isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
</Text> </Text>
</Animated.View> </View>
) : showStillFetching ? ( ) : showStillFetching ? (
<Animated.View <View
style={styles.loadingContainer} style={styles.loadingContainer}
> >
<MaterialIcons name="hourglass-bottom" size={32} color={colors.primary} /> <MaterialIcons name="hourglass-bottom" size={32} color={colors.primary} />
<Text style={styles.loadingText}>Still fetching streams</Text> <Text style={styles.loadingText}>Still fetching streams</Text>
</Animated.View> </View>
) : ( ) : (
// No streams and not loading = no streams available // No streams and not loading = no streams available
<Animated.View <View
style={styles.noStreams} style={styles.noStreams}
> >
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} /> <MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
<Text style={styles.noStreamsText}>No streams available</Text> <Text style={styles.noStreamsText}>No streams available</Text>
</Animated.View> </View>
) )
) : ( ) : (
// Show streams immediately when available, even if still loading others // Show streams immediately when available, even if still loading others
<View collapsable={false} style={{ flex: 1 }}> <View collapsable={false} style={{ flex: 1 }}>
{/* Show autoplay loading overlay if waiting for autoplay */} {/* Show autoplay loading overlay if waiting for autoplay */}
{isAutoplayWaiting && !autoplayTriggered && ( {isAutoplayWaiting && !autoplayTriggered && (
<Animated.View <View
style={styles.autoplayOverlay} style={styles.autoplayOverlay}
> >
<View style={styles.autoplayIndicator}> <View style={styles.autoplayIndicator}>
<ActivityIndicator size="small" color={colors.primary} /> <ActivityIndicator size="small" color={colors.primary} />
<Text style={styles.autoplayText}>Starting best stream...</Text> <Text style={styles.autoplayText}>Starting best stream...</Text>
</View> </View>
</Animated.View> </View>
)} )}
<SectionList <SectionList
@ -2134,17 +2172,6 @@ const createStyles = (colors: any) => StyleSheet.create({
fontSize: 13, fontSize: 13,
fontWeight: '600', fontWeight: '600',
}, },
transitionOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: colors.darkBackground,
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
},
sectionHeaderContainer: { sectionHeaderContainer: {
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 8, paddingVertical: 8,