mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
added backdrop gallery
This commit is contained in:
parent
1c7fd533c7
commit
81a7f63782
8 changed files with 404 additions and 14 deletions
|
|
@ -936,12 +936,12 @@ const styles = StyleSheet.create({
|
|||
|
||||
// Vertical Layout Styles
|
||||
episodeListContentVertical: {
|
||||
paddingBottom: 20,
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
episodeListContentVerticalTablet: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 20,
|
||||
paddingBottom: 8,
|
||||
},
|
||||
episodeGridVertical: {
|
||||
flexDirection: 'row',
|
||||
|
|
|
|||
|
|
@ -1692,6 +1692,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
if (__DEV__) console.log('[useMetadata] fetched certification via TMDB id (extract path)', { type, fetchedTmdbId, certification });
|
||||
setMetadata(prev => prev ? {
|
||||
...prev,
|
||||
tmdbId: fetchedTmdbId,
|
||||
certification
|
||||
} : null);
|
||||
} else {
|
||||
|
|
@ -1755,7 +1756,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
const cert = await tmdbSvc.getCertification(type, tmdbId);
|
||||
if (cert) {
|
||||
if (__DEV__) console.log('[useMetadata] fetched certification (attach path)', { type, tmdbId, cert });
|
||||
setMetadata(prev => prev ? { ...prev, certification: cert } : prev);
|
||||
setMetadata(prev => prev ? { ...prev, tmdbId, certification: cert } : prev);
|
||||
} else {
|
||||
if (__DEV__) console.warn('[useMetadata] TMDB returned no certification (attach path)', { type, tmdbId });
|
||||
}
|
||||
|
|
@ -1814,6 +1815,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Update metadata with TV details
|
||||
setMetadata((prev: any) => ({
|
||||
...prev,
|
||||
tmdbId,
|
||||
tvDetails
|
||||
}));
|
||||
}
|
||||
|
|
@ -1849,6 +1851,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
|||
// Update metadata with movie details
|
||||
setMetadata((prev: any) => ({
|
||||
...prev,
|
||||
tmdbId,
|
||||
movieDetails: movieDetailsObj
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,17 @@ export const useMetadataAssets = (
|
|||
// Optimized logo fetching
|
||||
useEffect(() => {
|
||||
const logoPreference = settings.logoSourcePreference || 'tmdb';
|
||||
|
||||
if (__DEV__) {
|
||||
console.log('[useMetadataAssets] Logo fetch triggered:', {
|
||||
id,
|
||||
type,
|
||||
logoPreference,
|
||||
hasImdbId: !!imdbId,
|
||||
tmdbEnrichmentEnabled: settings.enrichMetadataWithTMDB,
|
||||
logoFetchInProgress: logoFetchInProgress.current
|
||||
});
|
||||
}
|
||||
const currentLogoUrl = metadata?.logo;
|
||||
let shouldFetchLogo = false;
|
||||
|
||||
|
|
@ -141,12 +152,12 @@ export const useMetadataAssets = (
|
|||
|
||||
try {
|
||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||
|
||||
|
||||
if (logoPreference === 'tmdb') {
|
||||
// TMDB path - optimized flow
|
||||
let tmdbId: string | null = null;
|
||||
let contentType = type === 'series' ? 'tv' : 'movie';
|
||||
|
||||
|
||||
// Extract or find TMDB ID in one step
|
||||
if (id.startsWith('tmdb:')) {
|
||||
tmdbId = id.split(':')[1];
|
||||
|
|
@ -157,9 +168,11 @@ export const useMetadataAssets = (
|
|||
if (foundId) {
|
||||
tmdbId = String(foundId);
|
||||
setFoundTmdbId(tmdbId); // Save for banner fetching
|
||||
} else if (__DEV__) {
|
||||
console.log('[useMetadataAssets] Could not find TMDB ID for IMDB:', imdbId);
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
if (__DEV__) console.error('[useMetadataAssets] Error finding TMDB ID:', error);
|
||||
}
|
||||
} else {
|
||||
const parsedId = parseInt(id, 10);
|
||||
|
|
@ -173,15 +186,39 @@ export const useMetadataAssets = (
|
|||
// Direct fetch - avoid multiple service calls
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
const logoUrl = await tmdbService.getContentLogo(contentType as 'tv' | 'movie', tmdbId, preferredLanguage);
|
||||
|
||||
|
||||
if (__DEV__) {
|
||||
console.log('[useMetadataAssets] Logo fetch result:', {
|
||||
contentType,
|
||||
tmdbId,
|
||||
preferredLanguage,
|
||||
logoUrl,
|
||||
logoPreference
|
||||
});
|
||||
}
|
||||
|
||||
if (logoUrl) {
|
||||
// Preload the image
|
||||
FastImage.preload([{ uri: logoUrl }]);
|
||||
|
||||
|
||||
setMetadata((prevMetadata: any) => ({ ...prevMetadata!, logo: logoUrl }));
|
||||
} else {
|
||||
// TMDB logo not found, try to restore addon logo if it exists
|
||||
if (currentLogoUrl && !isTmdbUrl(currentLogoUrl)) {
|
||||
if (__DEV__) console.log('[useMetadataAssets] Restoring addon logo after TMDB logo not found');
|
||||
setMetadata((prevMetadata: any) => ({ ...prevMetadata!, logo: currentLogoUrl }));
|
||||
} else if (__DEV__) {
|
||||
console.log('[useMetadataAssets] No logo found for TMDB ID:', tmdbId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle error silently
|
||||
// TMDB logo fetch failed, try to restore addon logo if it exists
|
||||
if (currentLogoUrl && !isTmdbUrl(currentLogoUrl)) {
|
||||
if (__DEV__) console.log('[useMetadataAssets] Restoring addon logo after TMDB fetch error');
|
||||
setMetadata((prevMetadata: any) => ({ ...prevMetadata!, logo: currentLogoUrl }));
|
||||
} else if (__DEV__) {
|
||||
console.error('[useMetadataAssets] Logo fetch error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import CastMoviesScreen from '../screens/CastMoviesScreen';
|
|||
import UpdateScreen from '../screens/UpdateScreen';
|
||||
import AISettingsScreen from '../screens/AISettingsScreen';
|
||||
import AIChatScreen from '../screens/AIChatScreen';
|
||||
import BackdropGalleryScreen from '../screens/BackdropGalleryScreen';
|
||||
import BackupScreen from '../screens/BackupScreen';
|
||||
|
||||
// Stack navigator types
|
||||
|
|
@ -153,6 +154,11 @@ export type RootStackParamList = {
|
|||
episodeNumber?: number;
|
||||
title: string;
|
||||
};
|
||||
BackdropGallery: {
|
||||
tmdbId: number;
|
||||
type: 'movie' | 'tv';
|
||||
title: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
|
@ -1328,8 +1334,8 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AIChat"
|
||||
<Stack.Screen
|
||||
name="AIChat"
|
||||
component={AIChatScreen}
|
||||
options={{
|
||||
animation: Platform.OS === 'android' ? 'none' : 'slide_from_right',
|
||||
|
|
@ -1343,6 +1349,17 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
|
|||
},
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="BackdropGallery"
|
||||
component={BackdropGalleryScreen}
|
||||
options={{
|
||||
animation: 'slide_from_right',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</View>
|
||||
</PaperProvider>
|
||||
|
|
|
|||
254
src/screens/BackdropGalleryScreen.tsx
Normal file
254
src/screens/BackdropGalleryScreen.tsx
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
ActivityIndicator,
|
||||
StatusBar,
|
||||
} 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 { 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
|
||||
|
||||
interface BackdropItem {
|
||||
file_path: string;
|
||||
width: number;
|
||||
height: number;
|
||||
aspect_ratio: number;
|
||||
}
|
||||
|
||||
interface RouteParams {
|
||||
tmdbId: number;
|
||||
type: 'movie' | 'tv';
|
||||
title: string;
|
||||
}
|
||||
|
||||
const BackdropGalleryScreen: React.FC = () => {
|
||||
const route = useRoute();
|
||||
const navigation = useNavigation();
|
||||
const { tmdbId, type, title } = route.params as RouteParams;
|
||||
|
||||
const [backdrops, setBackdrops] = useState<BackdropItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBackdrops = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const tmdbService = TMDBService.getInstance();
|
||||
|
||||
let images;
|
||||
if (type === 'movie') {
|
||||
images = await tmdbService.getMovieImagesFull(tmdbId);
|
||||
} else {
|
||||
images = await tmdbService.getTvShowImagesFull(tmdbId);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
console.log('[BackdropGallery] TMDB response:', {
|
||||
tmdbId,
|
||||
type,
|
||||
hasImages: !!images,
|
||||
backdropsCount: images?.backdrops?.length || 0,
|
||||
images
|
||||
});
|
||||
}
|
||||
|
||||
if (images && images.backdrops && images.backdrops.length > 0) {
|
||||
setBackdrops(images.backdrops);
|
||||
} else {
|
||||
setError('No backdrops found');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load backdrops');
|
||||
console.error('Backdrop fetch error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (tmdbId) {
|
||||
fetchBackdrops();
|
||||
}
|
||||
}, [tmdbId, type]);
|
||||
|
||||
const renderBackdrop = ({ item, index }: { item: BackdropItem; index: number }) => {
|
||||
const imageUrl = `https://image.tmdb.org/t/p/w1280${item.file_path}`;
|
||||
|
||||
return (
|
||||
<View style={styles.backdropContainer}>
|
||||
<FastImage
|
||||
source={{ uri: imageUrl }}
|
||||
style={styles.backdropImage}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
<View style={styles.backdropInfo}>
|
||||
<Text style={styles.backdropResolution}>
|
||||
{item.width} × {item.height}
|
||||
</Text>
|
||||
<Text style={styles.backdropAspect}>
|
||||
{item.aspect_ratio.toFixed(2)}:1
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeader = () => (
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<MaterialIcons name="arrow-back" size={24} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title} numberOfLines={1}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{backdrops.length} Backdrop{backdrops.length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
|
||||
{renderHeader()}
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#fff" />
|
||||
<Text style={styles.loadingText}>Loading backdrops...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || backdrops.length === 0) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
|
||||
{renderHeader()}
|
||||
<View style={styles.errorContainer}>
|
||||
<MaterialIcons name="image-not-supported" size={64} color="rgba(255,255,255,0.5)" />
|
||||
<Text style={styles.errorText}>
|
||||
{error || 'No backdrops available'}
|
||||
</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
|
||||
{renderHeader()}
|
||||
<FlatList
|
||||
data={backdrops}
|
||||
keyExtractor={(item, index) => `${item.file_path}-${index}`}
|
||||
renderItem={renderBackdrop}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255,255,255,0.1)',
|
||||
},
|
||||
backButton: {
|
||||
padding: 8,
|
||||
marginRight: 12,
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
marginBottom: 2,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
},
|
||||
listContainer: {
|
||||
padding: 16,
|
||||
},
|
||||
backdropContainer: {
|
||||
marginBottom: 20,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(255,255,255,0.05)',
|
||||
},
|
||||
backdropImage: {
|
||||
width: BACKDROP_WIDTH,
|
||||
height: BACKDROP_HEIGHT,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
backdropInfo: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 12,
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
},
|
||||
backdropResolution: {
|
||||
fontSize: 12,
|
||||
color: '#fff',
|
||||
opacity: 0.8,
|
||||
},
|
||||
backdropAspect: {
|
||||
fontSize: 12,
|
||||
color: '#fff',
|
||||
opacity: 0.8,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
},
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 32,
|
||||
},
|
||||
errorText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
export default BackdropGalleryScreen;
|
||||
|
|
@ -995,6 +995,24 @@ const MetadataScreen: React.FC = () => {
|
|||
</View>
|
||||
)}
|
||||
|
||||
{/* Backdrop Gallery section - shown after details for movies/TV when TMDB ID is available */}
|
||||
{shouldLoadSecondaryData && metadata?.tmdbId && (
|
||||
<View style={styles.backdropGalleryContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.backdropGalleryButton}
|
||||
onPress={() => navigation.navigate('BackdropGallery' as any, {
|
||||
tmdbId: metadata.tmdbId,
|
||||
type: Object.keys(groupedEpisodes).length > 0 ? 'tv' : 'movie',
|
||||
title: metadata.name || 'Gallery'
|
||||
})}
|
||||
>
|
||||
<MaterialIcons name="photo-library" size={24} color="#fff" />
|
||||
<Text style={styles.backdropGalleryText}>Backdrop Gallery</Text>
|
||||
<MaterialIcons name="chevron-right" size={24} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Recommendations Section with skeleton when loading - Lazy loaded */}
|
||||
{type === 'movie' && shouldLoadSecondaryData && (
|
||||
<MemoizedMoreLikeThisSection
|
||||
|
|
@ -1287,7 +1305,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
tvDetailsContainer: {
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 20,
|
||||
marginTop: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
tvDetailsHeader: {
|
||||
|
|
@ -1321,6 +1339,30 @@ const styles = StyleSheet.create({
|
|||
textAlign: 'right',
|
||||
flex: 1,
|
||||
},
|
||||
backdropGalleryContainer: {
|
||||
paddingHorizontal: 16,
|
||||
marginTop: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
backdropGalleryButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: 'rgba(255,255,255,0.08)',
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
},
|
||||
backdropGalleryText: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
marginLeft: 12,
|
||||
opacity: 0.9,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export interface StreamingContent {
|
|||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
tmdbId?: number;
|
||||
poster: string;
|
||||
posterShape?: string;
|
||||
banner?: string;
|
||||
|
|
|
|||
|
|
@ -598,7 +598,25 @@ export class TMDBService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get movie images (logos, posters, backdrops) by TMDB ID
|
||||
* Get movie images (logos, posters, backdrops) by TMDB ID - returns full images object
|
||||
*/
|
||||
async getMovieImagesFull(movieId: number | string): Promise<any> {
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/movie/${movieId}/images`, {
|
||||
headers: await this.getHeaders(),
|
||||
params: await this.getParams({
|
||||
include_image_language: `en,null`
|
||||
}),
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get movie images (logos only) by TMDB ID - legacy method
|
||||
*/
|
||||
async getMovieImages(movieId: number | string, preferredLanguage: string = 'en'): Promise<string | null> {
|
||||
try {
|
||||
|
|
@ -697,7 +715,25 @@ export class TMDBService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get TV show images (logos, posters, backdrops) by TMDB ID
|
||||
* Get TV show images (logos, posters, backdrops) by TMDB ID - returns full images object
|
||||
*/
|
||||
async getTvShowImagesFull(showId: number | string): Promise<any> {
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/tv/${showId}/images`, {
|
||||
headers: await this.getHeaders(),
|
||||
params: await this.getParams({
|
||||
include_image_language: `en,null`
|
||||
}),
|
||||
});
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TV show images (logos only) by TMDB ID - legacy method
|
||||
*/
|
||||
async getTvShowImages(showId: number | string, preferredLanguage: string = 'en'): Promise<string | null> {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue