mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
added backdrop to set as default hero thumbnail
This commit is contained in:
parent
81a7f63782
commit
93221b9760
5 changed files with 204 additions and 14 deletions
|
|
@ -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<string | null>(null);
|
||||
const [nextLoadingQuality, setNextLoadingQuality] = useState<string | null>(null);
|
||||
const [nextLoadingTitle, setNextLoadingTitle] = useState<string | null>(null);
|
||||
|
||||
// Custom backdrop state
|
||||
const [customBackdropUrl, setCustomBackdropUrl] = useState<string | null>(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) && (
|
||||
<Animated.View style={[
|
||||
StyleSheet.absoluteFill,
|
||||
{
|
||||
|
|
@ -3130,7 +3145,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
|||
}
|
||||
]}>
|
||||
<Image
|
||||
source={{ uri: backdrop }}
|
||||
source={{ uri: customBackdropUrl || backdrop }}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
|
||||
const [isBuffering, setIsBuffering] = useState(false);
|
||||
const [ksAudioTracks, setKsAudioTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
|
||||
const [ksTextTracks, setKsTextTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
|
||||
|
|
@ -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) && (
|
||||
<Animated.View style={[
|
||||
StyleSheet.absoluteFill,
|
||||
{
|
||||
|
|
@ -2449,7 +2465,7 @@ const KSPlayerCore: React.FC = () => {
|
|||
}
|
||||
]}>
|
||||
<FastImage
|
||||
source={{ uri: backdrop }}
|
||||
source={{ uri: customBackdropUrl || backdrop }}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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<BackdropItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedBackdrop, setSelectedBackdrop] = useState<BackdropItem | null>(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 (
|
||||
<View style={styles.backdropContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.backdropContainer}
|
||||
onLongPress={() => {
|
||||
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}
|
||||
>
|
||||
<FastImage
|
||||
source={{ uri: imageUrl }}
|
||||
style={styles.backdropImage}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{isSelected && (
|
||||
<View style={styles.selectedIndicator}>
|
||||
<MaterialIcons name="check-circle" size={24} color="#fff" />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.backdropInfo}>
|
||||
<Text style={styles.backdropResolution}>
|
||||
{item.width} × {item.height}
|
||||
|
|
@ -100,7 +163,7 @@ const BackdropGalleryScreen: React.FC = () => {
|
|||
{item.aspect_ratio.toFixed(2)}:1
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -120,6 +183,23 @@ const BackdropGalleryScreen: React.FC = () => {
|
|||
{backdrops.length} Backdrop{backdrops.length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedBackdrop && (
|
||||
<TouchableOpacity
|
||||
style={styles.resetButton}
|
||||
onPress={() => {
|
||||
Alert.alert(
|
||||
'Reset Backdrop',
|
||||
'Remove custom backdrop and use default?',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'Reset', style: 'destructive', onPress: resetSelectedBackdrop }
|
||||
]
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="refresh" size={24} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
@ -155,6 +235,14 @@ const BackdropGalleryScreen: React.FC = () => {
|
|||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
|
||||
{renderHeader()}
|
||||
|
||||
{/* Explanatory note */}
|
||||
<View style={styles.noteContainer}>
|
||||
<Text style={styles.noteText}>
|
||||
Long press any backdrop to set it as your default for metadata screens and player loading overlay.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<FlatList
|
||||
data={backdrops}
|
||||
keyExtractor={(item, index) => `${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',
|
||||
|
|
|
|||
|
|
@ -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<any>(null);
|
||||
const [revealedSpoilers, setRevealedSpoilers] = useState<Set<string>>(new Set());
|
||||
|
||||
// Custom backdrop state
|
||||
const [customBackdropUrl, setCustomBackdropUrl] = useState<string | null>(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 */}
|
||||
<HeroSection
|
||||
<HeroSection
|
||||
metadata={metadata}
|
||||
bannerImage={assetData.bannerImage}
|
||||
bannerImage={customBackdropUrl || assetData.bannerImage}
|
||||
loadingBanner={assetData.loadingBanner}
|
||||
logoLoadError={assetData.logoLoadError}
|
||||
scrollY={animations.scrollY}
|
||||
|
|
|
|||
31
src/utils/backdropStorage.ts
Normal file
31
src/utils/backdropStorage.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
const SELECTED_BACKDROP_KEY = 'selected_custom_backdrop';
|
||||
|
||||
export interface SelectedBackdrop {
|
||||
file_path: string;
|
||||
width: number;
|
||||
height: number;
|
||||
aspect_ratio: number;
|
||||
}
|
||||
|
||||
export const getSelectedBackdrop = async (): Promise<SelectedBackdrop | null> => {
|
||||
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<string | null> => {
|
||||
const backdrop = await getSelectedBackdrop();
|
||||
if (backdrop) {
|
||||
return `https://image.tmdb.org/t/p/${size}${backdrop.file_path}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
Loading…
Reference in a new issue