From 93221b976065c0ca4d4358d442327ccddbc290f2 Mon Sep 17 00:00:00 2001 From: tapframe Date: Mon, 13 Oct 2025 13:45:40 +0530 Subject: [PATCH] added backdrop to set as default hero thumbnail --- src/components/player/AndroidVideoPlayer.tsx | 25 +++- src/components/player/KSPlayerCore.tsx | 26 +++- src/screens/BackdropGalleryScreen.tsx | 118 ++++++++++++++++++- src/screens/MetadataScreen.tsx | 18 ++- src/utils/backdropStorage.ts | 31 +++++ 5 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 src/utils/backdropStorage.ts diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index d0215b48..a36fe6c7 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -12,6 +12,7 @@ import { storageService } from '../../services/storageService'; import { logger } from '../../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; +import { getSelectedBackdropUrl } from '../../utils/backdropStorage'; import { LinearGradient } from 'expo-linear-gradient'; import { useTraktAutosync } from '../../hooks/useTraktAutosync'; import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; @@ -556,6 +557,9 @@ const AndroidVideoPlayer: React.FC = () => { const [nextLoadingProvider, setNextLoadingProvider] = useState(null); const [nextLoadingQuality, setNextLoadingQuality] = useState(null); const [nextLoadingTitle, setNextLoadingTitle] = useState(null); + + // Custom backdrop state + const [customBackdropUrl, setCustomBackdropUrl] = useState(null); const nextEpisodeButtonOpacity = useRef(new Animated.Value(0)).current; const nextEpisodeButtonScale = useRef(new Animated.Value(0.8)).current; @@ -579,16 +583,27 @@ const AndroidVideoPlayer: React.FC = () => { // Check if we have a logo to show const hasLogo = metadata && metadata.logo && !metadataLoading; + // Load custom backdrop on mount + useEffect(() => { + const loadCustomBackdrop = async () => { + const backdropUrl = await getSelectedBackdropUrl('original'); + setCustomBackdropUrl(backdropUrl); + }; + + loadCustomBackdrop(); + }, []); + // Prefetch backdrop and title logo for faster loading screen appearance useEffect(() => { - if (backdrop && typeof backdrop === 'string') { + const finalBackdrop = customBackdropUrl || backdrop; + if (finalBackdrop && typeof finalBackdrop === 'string') { // Reset loading state setIsBackdropLoaded(false); backdropImageOpacityAnim.setValue(0); // Prefetch the image try { - FastImage.preload([{ uri: backdrop }]); + FastImage.preload([{ uri: finalBackdrop }]); // Image prefetch initiated, fade it in smoothly setIsBackdropLoaded(true); Animated.timing(backdropImageOpacityAnim, { @@ -607,7 +622,7 @@ const AndroidVideoPlayer: React.FC = () => { setIsBackdropLoaded(true); backdropImageOpacityAnim.setValue(0); } - }, [backdrop]); + }, [backdrop, customBackdropUrl]); useEffect(() => { const logoUrl = (metadata && (metadata as any).logo) as string | undefined; @@ -3120,7 +3135,7 @@ const AndroidVideoPlayer: React.FC = () => { ]} pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'} > - {backdrop && ( + {(customBackdropUrl || backdrop) && ( { } ]}> diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 99426bdf..53bbcac4 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -11,6 +11,7 @@ import { storageService } from '../../services/storageService'; import { logger } from '../../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; +import { getSelectedBackdropUrl } from '../../utils/backdropStorage'; import { LinearGradient } from 'expo-linear-gradient'; import Slider from '@react-native-community/slider'; import KSPlayerComponent, { KSPlayerRef, KSPlayerSource } from './KSPlayerComponent'; @@ -134,6 +135,10 @@ const KSPlayerCore: React.FC = () => { const backgroundFadeAnim = useRef(new Animated.Value(1)).current; const [isBackdropLoaded, setIsBackdropLoaded] = useState(false); const backdropImageOpacityAnim = useRef(new Animated.Value(0)).current; + + // Custom backdrop state + const [customBackdropUrl, setCustomBackdropUrl] = useState(null); + const [isBuffering, setIsBuffering] = useState(false); const [ksAudioTracks, setKsAudioTracks] = useState>([]); const [ksTextTracks, setKsTextTracks] = useState>([]); @@ -310,16 +315,27 @@ const KSPlayerCore: React.FC = () => { // Check if we have a logo to show const hasLogo = metadata && metadata.logo && !metadataLoading; + // Load custom backdrop on mount + useEffect(() => { + const loadCustomBackdrop = async () => { + const backdropUrl = await getSelectedBackdropUrl('original'); + setCustomBackdropUrl(backdropUrl); + }; + + loadCustomBackdrop(); + }, []); + // Prefetch backdrop and title logo for faster loading screen appearance useEffect(() => { - if (backdrop && typeof backdrop === 'string') { + const finalBackdrop = customBackdropUrl || backdrop; + if (finalBackdrop && typeof finalBackdrop === 'string') { // Reset loading state setIsBackdropLoaded(false); backdropImageOpacityAnim.setValue(0); // Prefetch the image try { - FastImage.preload([{ uri: backdrop }]); + FastImage.preload([{ uri: finalBackdrop }]); // Image prefetch initiated, fade it in smoothly setIsBackdropLoaded(true); Animated.timing(backdropImageOpacityAnim, { @@ -338,7 +354,7 @@ const KSPlayerCore: React.FC = () => { setIsBackdropLoaded(true); backdropImageOpacityAnim.setValue(0); } - }, [backdrop]); + }, [backdrop, customBackdropUrl]); useEffect(() => { const logoUrl = (metadata && (metadata as any).logo) as string | undefined; @@ -2439,7 +2455,7 @@ const KSPlayerCore: React.FC = () => { ]} pointerEvents={shouldHideOpeningOverlay ? 'none' : 'auto'} > - {backdrop && ( + {(customBackdropUrl || backdrop) && ( { } ]}> diff --git a/src/screens/BackdropGalleryScreen.tsx b/src/screens/BackdropGalleryScreen.tsx index b80e2a4c..99f93438 100644 --- a/src/screens/BackdropGalleryScreen.tsx +++ b/src/screens/BackdropGalleryScreen.tsx @@ -8,17 +8,21 @@ import { Dimensions, ActivityIndicator, StatusBar, + Alert, } from 'react-native'; import { useRoute, useNavigation } from '@react-navigation/native'; import { SafeAreaView } from 'react-native-safe-area-context'; import FastImage from '@d11/react-native-fast-image'; import { MaterialIcons } from '@expo/vector-icons'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import { TMDBService } from '../services/tmdbService'; const { width } = Dimensions.get('window'); const BACKDROP_WIDTH = width * 0.9; const BACKDROP_HEIGHT = (BACKDROP_WIDTH * 9) / 16; // 16:9 aspect ratio +const SELECTED_BACKDROP_KEY = 'selected_custom_backdrop'; + interface BackdropItem { file_path: string; width: number; @@ -40,6 +44,7 @@ const BackdropGalleryScreen: React.FC = () => { const [backdrops, setBackdrops] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [selectedBackdrop, setSelectedBackdrop] = useState(null); useEffect(() => { const fetchBackdrops = async () => { @@ -82,16 +87,74 @@ const BackdropGalleryScreen: React.FC = () => { } }, [tmdbId, type]); + // Load selected backdrop from storage + useEffect(() => { + const loadSelectedBackdrop = async () => { + try { + const saved = await AsyncStorage.getItem(SELECTED_BACKDROP_KEY); + if (saved) { + const backdrop = JSON.parse(saved); + setSelectedBackdrop(backdrop); + } + } catch (error) { + console.error('Failed to load selected backdrop:', error); + } + }; + + loadSelectedBackdrop(); + }, []); + + const saveSelectedBackdrop = async (backdrop: BackdropItem) => { + try { + await AsyncStorage.setItem(SELECTED_BACKDROP_KEY, JSON.stringify(backdrop)); + setSelectedBackdrop(backdrop); + Alert.alert('Success', 'Custom backdrop set successfully!'); + } catch (error) { + console.error('Failed to save selected backdrop:', error); + Alert.alert('Error', 'Failed to save backdrop'); + } + }; + + const resetSelectedBackdrop = async () => { + try { + await AsyncStorage.removeItem(SELECTED_BACKDROP_KEY); + setSelectedBackdrop(null); + Alert.alert('Success', 'Custom backdrop reset to default!'); + } catch (error) { + console.error('Failed to reset selected backdrop:', error); + Alert.alert('Error', 'Failed to reset backdrop'); + } + }; + const renderBackdrop = ({ item, index }: { item: BackdropItem; index: number }) => { const imageUrl = `https://image.tmdb.org/t/p/w1280${item.file_path}`; + const isSelected = selectedBackdrop?.file_path === item.file_path; return ( - + { + Alert.alert( + 'Set as Default Backdrop', + 'Use this backdrop for metadata screens and player loading?', + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Set as Default', onPress: () => saveSelectedBackdrop(item) } + ] + ); + }} + delayLongPress={500} + > + {isSelected && ( + + + + )} {item.width} × {item.height} @@ -100,7 +163,7 @@ const BackdropGalleryScreen: React.FC = () => { {item.aspect_ratio.toFixed(2)}:1 - + ); }; @@ -120,6 +183,23 @@ const BackdropGalleryScreen: React.FC = () => { {backdrops.length} Backdrop{backdrops.length !== 1 ? 's' : ''} + {selectedBackdrop && ( + { + Alert.alert( + 'Reset Backdrop', + 'Remove custom backdrop and use default?', + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Reset', style: 'destructive', onPress: resetSelectedBackdrop } + ] + ); + }} + > + + + )} ); @@ -155,6 +235,14 @@ const BackdropGalleryScreen: React.FC = () => { {renderHeader()} + + {/* Explanatory note */} + + + Long press any backdrop to set it as your default for metadata screens and player loading overlay. + + + `${item.file_path}-${index}`} @@ -227,6 +315,32 @@ const styles = StyleSheet.create({ color: '#fff', opacity: 0.8, }, + selectedIndicator: { + position: 'absolute', + top: 12, + right: 12, + backgroundColor: 'rgba(0, 123, 255, 0.9)', + borderRadius: 12, + padding: 4, + }, + resetButton: { + padding: 8, + marginLeft: 12, + }, + noteContainer: { + paddingHorizontal: 16, + paddingVertical: 8, + backgroundColor: 'rgba(255,255,255,0.05)', + marginHorizontal: 16, + marginBottom: 8, + borderRadius: 8, + }, + noteText: { + fontSize: 12, + color: 'rgba(255,255,255,0.7)', + textAlign: 'center', + lineHeight: 16, + }, loadingContainer: { flex: 1, justifyContent: 'center', diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index dc587d2d..fe0dd0f8 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -46,6 +46,7 @@ import { useSettings } from '../hooks/useSettings'; import { MetadataLoadingScreen } from '../components/loading/MetadataLoadingScreen'; import { useTrailer } from '../contexts/TrailerContext'; import FastImage from '@d11/react-native-fast-image'; +import { getSelectedBackdropUrl } from '../utils/backdropStorage'; // Import our optimized components and hooks import HeroSection from '../components/metadata/HeroSection'; @@ -100,11 +101,24 @@ const MetadataScreen: React.FC = () => { const [selectedComment, setSelectedComment] = useState(null); const [revealedSpoilers, setRevealedSpoilers] = useState>(new Set()); + // Custom backdrop state + const [customBackdropUrl, setCustomBackdropUrl] = useState(null); + // Debug state changes React.useEffect(() => { console.log('MetadataScreen: commentBottomSheetVisible changed to:', commentBottomSheetVisible); }, [commentBottomSheetVisible]); + // Load custom backdrop on mount + useEffect(() => { + const loadCustomBackdrop = async () => { + const backdropUrl = await getSelectedBackdropUrl('original'); + setCustomBackdropUrl(backdropUrl); + }; + + loadCustomBackdrop(); + }, []); + React.useEffect(() => { console.log('MetadataScreen: selectedComment changed to:', selectedComment?.id); }, [selectedComment]); @@ -812,9 +826,9 @@ const MetadataScreen: React.FC = () => { contentContainerStyle={styles.scrollContent} > {/* Hero Section - Optimized */} - => { + try { + const saved = await AsyncStorage.getItem(SELECTED_BACKDROP_KEY); + if (saved) { + return JSON.parse(saved); + } + return null; + } catch (error) { + console.error('Failed to load selected backdrop:', error); + return null; + } +}; + +export const getSelectedBackdropUrl = async (size: 'original' | 'w1280' | 'w780' = 'original'): Promise => { + const backdrop = await getSelectedBackdrop(); + if (backdrop) { + return `https://image.tmdb.org/t/p/${size}${backdrop.file_path}`; + } + return null; +};