mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-30 14:28:55 +00:00
fixed audio selection issue
This commit is contained in:
parent
fa7352f4fa
commit
fa03d4455f
6 changed files with 344 additions and 143 deletions
1
KSPlayer
Submodule
1
KSPlayer
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 8fe5feb73ca3ee5092d2ed1dd8fcb692c10e3c11
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 5c020cca433f0400e23eb553f3e4de09f65b66d3
|
||||
Subproject commit 0a273ba5eb2632979a6678e005314c5b3ffb70eb
|
||||
|
|
@ -757,12 +757,35 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
|
||||
// Handle audio tracks
|
||||
if (data.audioTracks && data.audioTracks.length > 0) {
|
||||
const formattedAudioTracks = data.audioTracks.map((track: any, index: number) => ({
|
||||
id: track.index || index,
|
||||
name: track.title || track.language || `Audio ${index + 1}`,
|
||||
language: track.language,
|
||||
}));
|
||||
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(`[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);
|
||||
|
||||
// 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
|
||||
|
|
@ -1205,7 +1228,42 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
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);
|
||||
|
||||
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) => {
|
||||
|
|
@ -1737,6 +1795,20 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
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
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
|
|
|||
|
|
@ -751,7 +751,35 @@ const VideoPlayer: React.FC = () => {
|
|||
}
|
||||
|
||||
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) {
|
||||
setVlcTextTracks(data.textTracks);
|
||||
|
|
@ -1024,7 +1052,42 @@ const VideoPlayer: React.FC = () => {
|
|||
};
|
||||
|
||||
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);
|
||||
|
||||
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) => {
|
||||
|
|
@ -1578,6 +1641,20 @@ const VideoPlayer: React.FC = () => {
|
|||
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 newSize = Math.min(subtitleSize + 2, 32);
|
||||
saveSubtitleSize(newSize);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import Animated, {
|
|||
SlideInRight,
|
||||
SlideOutRight,
|
||||
} from 'react-native-reanimated';
|
||||
import { getTrackDisplayName } from '../utils/playerUtils';
|
||||
import { getTrackDisplayName, DEBUG_MODE } from '../utils/playerUtils';
|
||||
import { logger } from '../../../utils/logger';
|
||||
|
||||
interface AudioTrackModalProps {
|
||||
showAudioModal: boolean;
|
||||
|
|
@ -31,6 +32,20 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
|||
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;
|
||||
|
||||
return (
|
||||
|
|
@ -131,7 +146,9 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
|||
|
||||
<View style={{ gap: 8 }}>
|
||||
{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 (
|
||||
<TouchableOpacity
|
||||
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)',
|
||||
}}
|
||||
onPress={() => {
|
||||
if (DEBUG_MODE) {
|
||||
logger.log(`[AudioTrackModal] Selecting track: ${track.id} (${track.name})`);
|
||||
}
|
||||
selectAudioTrack(track.id);
|
||||
// Close modal after selection
|
||||
setTimeout(() => {
|
||||
setShowAudioModal(false);
|
||||
}, 200);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ import {
|
|||
Clipboard,
|
||||
Image as RNImage,
|
||||
} from 'react-native';
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
withDelay,
|
||||
runOnJS
|
||||
} from 'react-native-reanimated';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import * as ScreenOrientation from 'expo-screen-orientation';
|
||||
|
|
@ -38,22 +45,6 @@ import { localScraperService } from '../services/localScraperService';
|
|||
import { VideoPlayerService } from '../services/videoPlayerService';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
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';
|
||||
|
||||
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
|
||||
const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, theme, showLogos, scraperLogo }: {
|
||||
stream: Stream;
|
||||
|
|
@ -269,22 +356,20 @@ const ProviderFilter = memo(({
|
|||
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
|
||||
|
||||
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
|
||||
<Animated.View entering={FadeIn.duration(300).delay(index * 75)}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.filterChip,
|
||||
selectedProvider === item.id && styles.filterChipSelected
|
||||
]}
|
||||
onPress={() => onSelect(item.id)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.filterChipText,
|
||||
selectedProvider === item.id && styles.filterChipTextSelected
|
||||
]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.filterChip,
|
||||
selectedProvider === item.id && styles.filterChipSelected
|
||||
]}
|
||||
onPress={() => onSelect(item.id)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.filterChipText,
|
||||
selectedProvider === item.id && styles.filterChipTextSelected
|
||||
]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
), [selectedProvider, onSelect, styles]);
|
||||
|
||||
return (
|
||||
|
|
@ -383,10 +468,6 @@ export const StreamsScreen = () => {
|
|||
const [selectedProvider, setSelectedProvider] = React.useState('all');
|
||||
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
|
||||
const [loadingProviders, setLoadingProviders] = useState<{[key: string]: boolean}>({});
|
||||
|
|
@ -571,34 +652,9 @@ export const StreamsScreen = () => {
|
|||
checkProviders();
|
||||
}, [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
|
||||
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') {
|
||||
// Reset stack to ensure there is always a screen to go back to from Metadata
|
||||
(navigation as any).reset({
|
||||
|
|
@ -616,7 +672,7 @@ export const StreamsScreen = () => {
|
|||
} else {
|
||||
(navigation as any).navigate('MainTabs');
|
||||
}
|
||||
}, [navigation, headerOpacity, heroScale, filterOpacity, type, id]);
|
||||
}, [navigation, type, id]);
|
||||
|
||||
const handleProviderChange = useCallback((provider: string) => {
|
||||
setSelectedProvider(provider);
|
||||
|
|
@ -1430,24 +1486,6 @@ export const StreamsScreen = () => {
|
|||
const showInitialLoading = streamsEmpty && (streamsLoadStart === null || 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 stream = item;
|
||||
|
|
@ -1509,7 +1547,7 @@ export const StreamsScreen = () => {
|
|||
|
||||
|
||||
{Platform.OS !== 'ios' && (
|
||||
<Animated.View
|
||||
<View
|
||||
style={[styles.backButtonContainer]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
|
|
@ -1525,38 +1563,37 @@ export const StreamsScreen = () => {
|
|||
{type === 'series' ? 'Back to Episodes' : 'Back to Info'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{type === 'movie' && metadata && (
|
||||
<Animated.View style={[styles.movieTitleContainer, heroStyle]}>
|
||||
<View style={[styles.movieTitleContainer]}>
|
||||
<View style={styles.movieTitleContent}>
|
||||
{metadata.logo ? (
|
||||
<Image
|
||||
<AnimatedImage
|
||||
source={{ uri: metadata.logo }}
|
||||
style={styles.movieLogo}
|
||||
contentFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<Text style={styles.movieTitle} numberOfLines={2}>
|
||||
<AnimatedText style={styles.movieTitle} numberOfLines={2}>
|
||||
{metadata.name}
|
||||
</Text>
|
||||
</AnimatedText>
|
||||
)}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{type === 'series' && (
|
||||
<Animated.View style={[styles.streamsHeroContainer, heroStyle]}>
|
||||
<Animated.View style={StyleSheet.absoluteFill}>
|
||||
<Animated.View
|
||||
<View style={[styles.streamsHeroContainer]}>
|
||||
<View style={StyleSheet.absoluteFill}>
|
||||
<View
|
||||
style={StyleSheet.absoluteFill}
|
||||
>
|
||||
<Image
|
||||
<AnimatedImage
|
||||
source={episodeImage ? { uri: episodeImage } : undefined}
|
||||
style={styles.streamsHeroBackground}
|
||||
contentFit="cover"
|
||||
transition={500}
|
||||
/>
|
||||
<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]}
|
||||
|
|
@ -1565,58 +1602,60 @@ export const StreamsScreen = () => {
|
|||
>
|
||||
<View style={styles.streamsHeroContent}>
|
||||
{currentEpisode ? (
|
||||
<Animated.View style={styles.streamsHeroInfo}>
|
||||
<Text style={styles.streamsHeroEpisodeNumber}>{currentEpisode.episodeString}</Text>
|
||||
<Text style={styles.streamsHeroTitle} numberOfLines={1}>
|
||||
<View style={styles.streamsHeroInfo}>
|
||||
<AnimatedText style={styles.streamsHeroEpisodeNumber} delay={50}>
|
||||
{currentEpisode.episodeString}
|
||||
</AnimatedText>
|
||||
<AnimatedText style={styles.streamsHeroTitle} numberOfLines={1} delay={100}>
|
||||
{currentEpisode.name}
|
||||
</Text>
|
||||
</AnimatedText>
|
||||
{!!currentEpisode.overview && (
|
||||
<Animated.View>
|
||||
<AnimatedView delay={150}>
|
||||
<Text style={styles.streamsHeroOverview} numberOfLines={2}>
|
||||
{currentEpisode.overview}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</AnimatedView>
|
||||
)}
|
||||
<Animated.View style={styles.streamsHeroMeta}>
|
||||
<AnimatedView style={styles.streamsHeroMeta} delay={200}>
|
||||
<Text style={styles.streamsHeroReleased}>
|
||||
{tmdbService.formatAirDate(currentEpisode.air_date)}
|
||||
</Text>
|
||||
{effectiveEpisodeVote > 0 && (
|
||||
<Animated.View style={styles.streamsHeroRating}>
|
||||
<View style={styles.streamsHeroRating}>
|
||||
<Image source={{ uri: TMDB_LOGO }} style={styles.tmdbLogo} contentFit="contain" />
|
||||
<Text style={styles.streamsHeroRatingText}>
|
||||
{effectiveEpisodeVote.toFixed(1)}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
{!!effectiveEpisodeRuntime && (
|
||||
<Animated.View style={styles.streamsHeroRuntime}>
|
||||
<View style={styles.streamsHeroRuntime}>
|
||||
<MaterialIcons name="schedule" size={16} color={colors.mediumEmphasis} />
|
||||
<Text style={styles.streamsHeroRuntimeText}>
|
||||
{effectiveEpisodeRuntime >= 60
|
||||
? `${Math.floor(effectiveEpisodeRuntime / 60)}h ${effectiveEpisodeRuntime % 60}m`
|
||||
: `${effectiveEpisodeRuntime}m`}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</AnimatedView>
|
||||
</View>
|
||||
) : (
|
||||
// Placeholder to reserve space and avoid layout shift while loading
|
||||
<View style={{ width: '100%', height: 120 }} />
|
||||
)}
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={[
|
||||
styles.streamsMainContent,
|
||||
type === 'movie' && styles.streamsMainContentMovie
|
||||
]}>
|
||||
<Animated.View style={[styles.filterContainer, filterStyle]}>
|
||||
<View style={[styles.filterContainer]}>
|
||||
{Object.keys(streams).length > 0 && (
|
||||
<ProviderFilter
|
||||
selectedProvider={selectedProvider}
|
||||
|
|
@ -1625,11 +1664,11 @@ export const StreamsScreen = () => {
|
|||
theme={currentTheme}
|
||||
/>
|
||||
)}
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* Active Scrapers Status */}
|
||||
{activeFetchingScrapers.length > 0 && (
|
||||
<Animated.View
|
||||
<View
|
||||
style={styles.activeScrapersContainer}
|
||||
>
|
||||
<Text style={styles.activeScrapersTitle}>Fetching from:</Text>
|
||||
|
|
@ -1638,13 +1677,12 @@ export const StreamsScreen = () => {
|
|||
<PulsingChip key={scraperName} text={scraperName} delay={index * 200} />
|
||||
))}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Update the streams/loading state display logic */}
|
||||
{ showNoSourcesError ? (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(300)}
|
||||
<View
|
||||
style={styles.noStreams}
|
||||
>
|
||||
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
||||
|
|
@ -1658,46 +1696,46 @@ export const StreamsScreen = () => {
|
|||
>
|
||||
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
) : streamsEmpty ? (
|
||||
showInitialLoading ? (
|
||||
<Animated.View
|
||||
<View
|
||||
style={styles.loadingContainer}
|
||||
>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={styles.loadingText}>
|
||||
{isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
) : showStillFetching ? (
|
||||
<Animated.View
|
||||
<View
|
||||
style={styles.loadingContainer}
|
||||
>
|
||||
<MaterialIcons name="hourglass-bottom" size={32} color={colors.primary} />
|
||||
<Text style={styles.loadingText}>Still fetching streams…</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
) : (
|
||||
// No streams and not loading = no streams available
|
||||
<Animated.View
|
||||
<View
|
||||
style={styles.noStreams}
|
||||
>
|
||||
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
||||
<Text style={styles.noStreamsText}>No streams available</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
// Show streams immediately when available, even if still loading others
|
||||
<View collapsable={false} style={{ flex: 1 }}>
|
||||
{/* Show autoplay loading overlay if waiting for autoplay */}
|
||||
{isAutoplayWaiting && !autoplayTriggered && (
|
||||
<Animated.View
|
||||
<View
|
||||
style={styles.autoplayOverlay}
|
||||
>
|
||||
<View style={styles.autoplayIndicator}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<Text style={styles.autoplayText}>Starting best stream...</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<SectionList
|
||||
|
|
@ -2134,17 +2172,6 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
},
|
||||
transitionOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: colors.darkBackground,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 9999,
|
||||
},
|
||||
sectionHeaderContainer: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
|
|
|
|||
Loading…
Reference in a new issue