trailer bug fixes

This commit is contained in:
tapframe 2025-11-08 13:03:20 +05:30
parent ea7f6bf7d7
commit aa0c338c05
6 changed files with 221 additions and 105 deletions

View file

@ -0,0 +1,49 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
export type AgeRating =
| 'NC-17'
| 'TV-Y'
| 'TV-Y7'
| 'G'
| 'TV-G'
| 'PG'
| 'TV-PG'
| 'PG-13'
| 'TV-14'
| 'R'
| 'TV-MA';
interface AgeRatingBadgeProps {
rating: AgeRating | string;
}
const AgeRatingBadge: React.FC<AgeRatingBadgeProps> = ({ rating }) => {
if (!rating) return null;
return (
<View style={styles.container}>
<Text style={styles.text}>{rating}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: '#575757',
paddingHorizontal: 6,
paddingVertical: 3,
borderRadius: 2,
alignItems: 'center',
justifyContent: 'center',
},
text: {
color: '#E6E6E6',
fontSize: 10,
fontWeight: '600',
letterSpacing: 0.6,
textTransform: 'uppercase',
},
});
export default AgeRatingBadge;

View file

@ -9,12 +9,12 @@ import {
ViewStyle,
TextStyle,
StatusBar,
Image,
} from 'react-native';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { NavigationProp, useNavigation, useIsFocused } from '@react-navigation/native';
import { RootStackParamList } from '../../navigation/AppNavigator';
import { LinearGradient } from 'expo-linear-gradient';
import FastImage from '@d11/react-native-fast-image';
import { SvgUri } from 'react-native-svg';
import { MaterialIcons, Entypo } from '@expo/vector-icons';
import Animated, {
FadeIn,
@ -26,7 +26,7 @@ import Animated, {
withDelay,
runOnJS,
interpolate,
Extrapolate,
Extrapolation,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { StreamingContent } from '../../services/catalogService';
@ -88,6 +88,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
onRetry,
}) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isFocused = useIsFocused();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const { settings, updateSetting } = useSettings();
@ -105,6 +106,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
const [bannerLoaded, setBannerLoaded] = useState<Record<number, boolean>>({});
const [logoLoaded, setLogoLoaded] = useState<Record<number, boolean>>({});
const [logoError, setLogoError] = useState<Record<number, boolean>>({});
const [logoHeights, setLogoHeights] = useState<Record<number, number>>({});
const autoPlayTimerRef = useRef<NodeJS.Timeout | null>(null);
const lastInteractionRef = useRef<number>(Date.now());
@ -114,6 +116,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
const [trailerError, setTrailerError] = useState(false);
const [trailerReady, setTrailerReady] = useState(false);
const [trailerPreloaded, setTrailerPreloaded] = useState(false);
const [trailerShouldBePaused, setTrailerShouldBePaused] = useState(false);
const trailerVideoRef = useRef<any>(null);
// Use ref to avoid re-fetching trailer when trailerMuted changes
@ -162,8 +165,53 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
setBannerLoaded({});
setLogoLoaded({});
setLogoError({});
setLogoHeights({});
}, [items.length]);
// Stop trailer when screen loses focus
useEffect(() => {
if (!isFocused) {
// Pause this screen's trailer
setTrailerShouldBePaused(true);
setTrailerPlaying(false);
// Fade out trailer
trailerOpacity.value = withTiming(0, { duration: 300 });
thumbnailOpacity.value = withTiming(1, { duration: 300 });
logger.info('[AppleTVHero] Screen lost focus - pausing trailer');
} else {
// Screen gained focus - allow trailer to resume if it was ready
setTrailerShouldBePaused(false);
// If trailer was ready and loaded, restore the video opacity
if (trailerReady && trailerUrl) {
logger.info('[AppleTVHero] Screen gained focus - restoring trailer');
thumbnailOpacity.value = withTiming(0, { duration: 800 });
trailerOpacity.value = withTiming(1, { duration: 800 });
setTrailerPlaying(true);
}
}
}, [isFocused, setTrailerPlaying, trailerOpacity, thumbnailOpacity, trailerReady, trailerUrl]);
// Listen to navigation events to stop trailer when navigating to other screens
useEffect(() => {
const unsubscribe = navigation.addListener('blur', () => {
// Screen is blurred (navigated away)
setTrailerPlaying(false);
trailerOpacity.value = withTiming(0, { duration: 300 });
thumbnailOpacity.value = withTiming(1, { duration: 300 });
logger.info('[AppleTVHero] Navigation blur event - stopping trailer');
});
return () => {
unsubscribe();
// Stop trailer when component unmounts
setTrailerPlaying(false);
logger.info('[AppleTVHero] Component unmounting - stopping trailer');
};
}, [navigation, setTrailerPlaying, trailerOpacity, thumbnailOpacity]);
// Fetch trailer URL when current item changes
useEffect(() => {
let alive = true;
@ -342,12 +390,12 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
dragProgress.value = 0;
setNextIndex(currentIndex);
// Quick logo fade
// Faster logo fade
logoOpacity.value = 0;
logoOpacity.value = withDelay(
150,
80,
withTiming(1, {
duration: 400,
duration: 250,
easing: Easing.out(Easing.cubic),
})
);
@ -385,8 +433,8 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
})
.onUpdate((event) => {
const translationX = event.translationX;
// Use smaller width multiplier for easier drag
const progress = Math.abs(translationX) / (width * 0.6);
// Use larger width multiplier for smoother visual feedback on small swipes
const progress = Math.abs(translationX) / (width * 1.2);
// Update drag progress (0 to 1) with eased curve
dragProgress.value = Math.min(progress, 1);
@ -412,20 +460,31 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
.onEnd((event) => {
const velocity = event.velocityX;
const translationX = event.translationX;
const swipeThreshold = width * 0.15; // Smaller threshold - easier to swipe
const swipeThreshold = width * 0.05; // Very small threshold - minimal swipe needed
if (Math.abs(translationX) > swipeThreshold || Math.abs(velocity) > 600) {
// Complete the swipe - instant navigation
if (translationX > 0) {
runOnJS(goToPrevious)();
} else {
runOnJS(goToNext)();
}
if (Math.abs(translationX) > swipeThreshold || Math.abs(velocity) > 300) {
// Complete the swipe - animate to full opacity before navigation
dragProgress.value = withTiming(
1,
{
duration: 300,
easing: Easing.out(Easing.cubic),
},
(finished) => {
if (finished) {
if (translationX > 0) {
runOnJS(goToPrevious)();
} else {
runOnJS(goToNext)();
}
}
}
);
} else {
// Cancel the swipe - animate back with ease
// Cancel the swipe - animate back with smooth ease
dragProgress.value = withTiming(0, {
duration: 250,
easing: Easing.bezier(0.4, 0.0, 0.2, 1), // Material design ease curve
duration: 450,
easing: Easing.bezier(0.25, 0.1, 0.25, 1), // Custom ease-out for buttery smooth return
});
}
}),
@ -434,21 +493,28 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
// Animated styles for next image only - smooth crossfade + slide during drag
const nextImageStyle = useAnimatedStyle(() => {
// Enhanced 4-point curve for smoother crossfade
// Silky-smooth 10-point ease curve for cinematic crossfade
const opacity = interpolate(
dragProgress.value,
[0, 0.3, 0.7, 1],
[0, 0.4, 0.8, 1],
Extrapolate.CLAMP
[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.85, 1],
[0, 0.05, 0.12, 0.22, 0.35, 0.5, 0.65, 0.78, 0.92, 1],
Extrapolation.CLAMP
);
// Smoother slide effect with ease-out curve
const slideDistance = 20; // Subtle 20px movement
// Ultra-subtle slide effect with smooth ease-out curve
const slideDistance = 6; // Even more subtle 6px movement
const slideProgress = interpolate(
dragProgress.value,
[0, 0.4, 0.8, 1], // 4-point for smoother acceleration
[-slideDistance * dragDirection.value, -slideDistance * 0.5 * dragDirection.value, -slideDistance * 0.15 * dragDirection.value, 0],
Extrapolate.CLAMP
[0, 0.2, 0.4, 0.6, 0.8, 1], // 6-point for ultra-smooth acceleration
[
-slideDistance * dragDirection.value,
-slideDistance * 0.8 * dragDirection.value,
-slideDistance * 0.6 * dragDirection.value,
-slideDistance * 0.35 * dragDirection.value,
-slideDistance * 0.12 * dragDirection.value,
0
],
Extrapolation.CLAMP
);
return {
@ -463,7 +529,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
dragProgress.value,
[0, 0.2, 0.4],
[1, 0.5, 0],
Extrapolate.CLAMP
Extrapolation.CLAMP
);
return {
@ -560,6 +626,8 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
hideLoadingSpinner={true}
onLoad={handleTrailerPreloaded}
onError={handleTrailerError}
contentType={currentItem.type as 'movie' | 'series'}
paused={true}
/>
</View>
)}
@ -581,6 +649,8 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
onLoad={handleTrailerReady}
onError={handleTrailerError}
onEnd={handleTrailerEnd}
contentType={currentItem.type as 'movie' | 'series'}
paused={trailerShouldBePaused}
onPlaybackStatusUpdate={(status) => {
if (status.isLoaded && !trailerReady) {
handleTrailerReady();
@ -683,34 +753,28 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
style={logoAnimatedStyle}
>
{currentItem.logo && !logoError[currentIndex] ? (
<View style={styles.logoContainer}>
{currentItem.logo.toLowerCase().endsWith('.svg') ? (
<SvgUri
uri={currentItem.logo}
width="100%"
height="100%"
onLoad={() => setLogoLoaded((prev) => ({ ...prev, [currentIndex]: true }))}
onError={() => {
setLogoError((prev) => ({ ...prev, [currentIndex]: true }));
logger.warn('[AppleTVHero] SVG Logo load failed:', currentItem.logo);
}}
/>
) : (
<FastImage
source={{
uri: currentItem.logo,
priority: FastImage.priority.high,
cache: FastImage.cacheControl.immutable,
}}
style={styles.logo}
resizeMode={FastImage.resizeMode.contain}
onLoad={() => setLogoLoaded((prev) => ({ ...prev, [currentIndex]: true }))}
onError={() => {
setLogoError((prev) => ({ ...prev, [currentIndex]: true }));
logger.warn('[AppleTVHero] Logo load failed:', currentItem.logo);
}}
/>
)}
<View
style={[
styles.logoContainer,
logoHeights[currentIndex] && logoHeights[currentIndex] < 80
? { marginBottom: 4 } // Minimal spacing for small logos
: { marginBottom: 8 } // Small spacing for normal logos
]}
onLayout={(event) => {
const { height } = event.nativeEvent.layout;
setLogoHeights((prev) => ({ ...prev, [currentIndex]: height }));
}}
>
<Image
source={{ uri: currentItem.logo }}
style={styles.logo}
resizeMode="contain"
onLoad={() => setLogoLoaded((prev) => ({ ...prev, [currentIndex]: true }))}
onError={() => {
setLogoError((prev) => ({ ...prev, [currentIndex]: true }));
logger.warn('[AppleTVHero] Logo load failed:', currentItem.logo);
}}
/>
</View>
) : (
<View style={styles.titleContainer}>
@ -734,11 +798,6 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
<Text style={styles.metadataText}>{currentItem.genres[0]}</Text>
</>
)}
{currentItem.certification && (
<View style={styles.ratingBadge}>
<Text style={styles.ratingText}>{currentItem.certification}</Text>
</View>
)}
</View>
</View>
@ -837,8 +896,7 @@ const styles = StyleSheet.create({
},
logoContainer: {
width: width * 0.6,
height: 120,
marginBottom: 20,
height: 100,
alignItems: 'center',
justifyContent: 'center',
},
@ -847,7 +905,7 @@ const styles = StyleSheet.create({
height: '100%',
},
titleContainer: {
marginBottom: 20,
marginBottom: 8,
alignItems: 'center',
justifyContent: 'center',
},
@ -861,7 +919,7 @@ const styles = StyleSheet.create({
textShadowRadius: 4,
},
metadataContainer: {
marginBottom: 24,
marginBottom: 20,
alignItems: 'center',
justifyContent: 'center',
},
@ -869,10 +927,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(255,255,255,0.15)',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
gap: 8,
},
metadataText: {
@ -884,18 +938,6 @@ const styles = StyleSheet.create({
color: 'rgba(255,255,255,0.6)',
fontSize: 14,
},
ratingBadge: {
backgroundColor: 'rgba(255,255,255,0.25)',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 4,
marginLeft: 4,
},
ratingText: {
color: '#fff',
fontSize: 12,
fontWeight: '700',
},
buttonsContainer: {
flexDirection: 'row',
alignItems: 'center',

View file

@ -21,6 +21,7 @@ import Animated, {
import { useTheme } from '../../contexts/ThemeContext';
import { isMDBListEnabled } from '../../screens/MDBListSettingsScreen';
import { getAgeRatingColor } from '../../utils/ageRatingColors';
import AgeRatingBadge from '../common/AgeRatingBadge';
// Enhanced responsive breakpoints for Metadata Details
const BREAKPOINTS = {
@ -215,14 +216,7 @@ function formatRuntime(runtime: string): string {
</Text>
)}
{metadata.certification && (
<Text style={[
styles.metaText,
styles.premiumOutlinedText,
{
color: getAgeRatingColor(metadata.certification, type === 'series' ? 'series' : 'movie'),
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15
}
]}>{metadata.certification}</Text>
<AgeRatingBadge rating={metadata.certification} />
)}
{metadata.imdbRating && !isMDBEnabled && (
<View style={styles.ratingContainer}>

View file

@ -40,6 +40,8 @@ interface TrailerPlayerProps {
hideLoadingSpinner?: boolean;
onFullscreenToggle?: () => void;
hideControls?: boolean;
contentType?: 'movie' | 'series';
paused?: boolean; // External control to pause/play
}
const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
@ -56,6 +58,8 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
hideLoadingSpinner = false,
onFullscreenToggle,
hideControls = false,
contentType = 'movie',
paused,
}, ref) => {
const { currentTheme } = useTheme();
const { isTrailerPlaying: globalTrailerPlaying } = useTrailer();
@ -142,27 +146,42 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
}, [cleanupVideo]);
// Handle autoPlay prop changes to keep internal state synchronized
// But only if no external paused prop is provided
useEffect(() => {
if (isComponentMounted) {
if (isComponentMounted && paused === undefined) {
setIsPlaying(autoPlay);
}
}, [autoPlay, isComponentMounted]);
}, [autoPlay, isComponentMounted, paused]);
// Respond to global trailer state changes (e.g., when modal opens)
// Handle muted prop changes to keep internal state synchronized
useEffect(() => {
if (isComponentMounted) {
// If global trailer is paused, pause this trailer too
if (!globalTrailerPlaying && isPlaying) {
setIsMuted(muted);
}
}, [muted, isComponentMounted]);
// Handle external paused prop to override playing state (highest priority)
useEffect(() => {
if (paused !== undefined) {
setIsPlaying(!paused);
logger.info('TrailerPlayer', `External paused prop changed: ${paused}, setting isPlaying to ${!paused}`);
}
}, [paused]);
// Respond to global trailer state changes (e.g., when modal opens)
// Only apply if no external paused prop is controlling this
useEffect(() => {
if (isComponentMounted && paused === undefined) {
// Always sync with global trailer state when pausing
// This ensures all trailers pause when one screen loses focus
if (!globalTrailerPlaying) {
logger.info('TrailerPlayer', 'Global trailer paused - pausing this trailer');
setIsPlaying(false);
}
// If global trailer is resumed and autoPlay is enabled, resume this trailer
else if (globalTrailerPlaying && !isPlaying && autoPlay) {
logger.info('TrailerPlayer', 'Global trailer resumed - resuming this trailer');
setIsPlaying(true);
}
// Don't automatically resume from global state
// Each trailer should manage its own resume logic based on its screen focus
}
}, [globalTrailerPlaying, isPlaying, autoPlay, isComponentMounted]);
}, [globalTrailerPlaying, isComponentMounted, paused]);
const showControlsWithTimeout = useCallback(() => {
if (!isComponentMounted) return;
@ -360,8 +379,11 @@ const TrailerPlayer = React.forwardRef<any, TrailerPlayerProps>(({
}
return { uri: trailerUrl } as any;
})()}
style={styles.video}
resizeMode={isFullscreen ? 'contain' : 'cover'}
style={[
styles.video,
contentType === 'movie' && styles.movieVideoScale,
]}
resizeMode="cover"
paused={!isPlaying}
repeat={false}
muted={isMuted}
@ -491,6 +513,9 @@ const styles = StyleSheet.create({
width: '100%',
height: '100%',
},
movieVideoScale: {
transform: [{ scale: 1.30 }], // Custom scale for movies to crop black bars
},
videoOverlay: {
position: 'absolute',
top: 0,

View file

@ -873,6 +873,7 @@ const MainTabs = () => {
options={{
title: 'Home',
tabBarIcon: () => ({ sfSymbol: 'house' }),
freezeOnBlur: true,
}}
/>
<IOSTab.Screen
@ -977,6 +978,7 @@ const MainTabs = () => {
tabBarIcon: ({ color, size, focused }) => (
<MaterialCommunityIcons name={focused ? 'home' : 'home-outline'} size={size} color={color} />
),
freezeOnBlur: true,
}}
/>
<Tab.Screen

View file

@ -64,6 +64,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useToast } from '../contexts/ToastContext';
import FirstTimeWelcome from '../components/FirstTimeWelcome';
import { HeaderVisibility } from '../contexts/HeaderVisibility';
import { useTrailer } from '../contexts/TrailerContext';
// Constants
const CATALOG_SETTINGS_KEY = 'catalog_settings';
@ -119,6 +120,7 @@ const HomeScreen = () => {
const { settings } = useSettings();
const { lastUpdate } = useCatalogContext(); // Add catalog context to listen for addon changes
const { showInfo } = useToast();
const { setTrailerPlaying } = useTrailer();
const [showHeroSection, setShowHeroSection] = useState(settings.showHeroSection);
const [featuredContentSource, setFeaturedContentSource] = useState(settings.featuredContentSource);
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@ -411,9 +413,11 @@ const HomeScreen = () => {
ScreenOrientation.unlockAsync().catch(() => {});
return () => {
// Keep translucent when unfocusing to prevent layout shifts
// Stop trailer when screen loses focus (navigating to other screens)
setTrailerPlaying(false);
logger.info('[HomeScreen] Screen blur - stopping trailer');
};
}, [])
}, [setTrailerPlaying])
);
// Handle app state changes for smart cache management