added backdrop gallery

This commit is contained in:
tapframe 2025-10-13 13:36:50 +05:30
parent 1c7fd533c7
commit 81a7f63782
8 changed files with 404 additions and 14 deletions

View file

@ -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',

View file

@ -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
}));
}

View file

@ -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);
}
}
}
}

View file

@ -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>

View 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;

View file

@ -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,
},
});

View file

@ -54,6 +54,7 @@ export interface StreamingContent {
id: string;
type: string;
name: string;
tmdbId?: number;
poster: string;
posterShape?: string;
banner?: string;

View file

@ -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 {