mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-24 02:02:09 +00:00
UI fix
This commit is contained in:
parent
84a308e5dc
commit
2599fd85d7
7 changed files with 120 additions and 211 deletions
|
|
@ -12,7 +12,6 @@ import { storageService } from '../../services/storageService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { getSelectedBackdropUrl } from '../../utils/backdropStorage';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
||||||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||||
|
|
@ -558,8 +557,6 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const [nextLoadingQuality, setNextLoadingQuality] = useState<string | null>(null);
|
const [nextLoadingQuality, setNextLoadingQuality] = useState<string | null>(null);
|
||||||
const [nextLoadingTitle, setNextLoadingTitle] = 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 nextEpisodeButtonOpacity = useRef(new Animated.Value(0)).current;
|
||||||
const nextEpisodeButtonScale = useRef(new Animated.Value(0.8)).current;
|
const nextEpisodeButtonScale = useRef(new Animated.Value(0.8)).current;
|
||||||
|
|
||||||
|
|
@ -583,27 +580,16 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
// Check if we have a logo to show
|
// Check if we have a logo to show
|
||||||
const hasLogo = metadata && metadata.logo && !metadataLoading;
|
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
|
// Prefetch backdrop and title logo for faster loading screen appearance
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const finalBackdrop = customBackdropUrl || backdrop;
|
if (backdrop && typeof backdrop === 'string') {
|
||||||
if (finalBackdrop && typeof finalBackdrop === 'string') {
|
|
||||||
// Reset loading state
|
// Reset loading state
|
||||||
setIsBackdropLoaded(false);
|
setIsBackdropLoaded(false);
|
||||||
backdropImageOpacityAnim.setValue(0);
|
backdropImageOpacityAnim.setValue(0);
|
||||||
|
|
||||||
// Prefetch the image
|
// Prefetch the image
|
||||||
try {
|
try {
|
||||||
FastImage.preload([{ uri: finalBackdrop }]);
|
FastImage.preload([{ uri: backdrop }]);
|
||||||
// Image prefetch initiated, fade it in smoothly
|
// Image prefetch initiated, fade it in smoothly
|
||||||
setIsBackdropLoaded(true);
|
setIsBackdropLoaded(true);
|
||||||
Animated.timing(backdropImageOpacityAnim, {
|
Animated.timing(backdropImageOpacityAnim, {
|
||||||
|
|
@ -622,7 +608,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
setIsBackdropLoaded(true);
|
setIsBackdropLoaded(true);
|
||||||
backdropImageOpacityAnim.setValue(0);
|
backdropImageOpacityAnim.setValue(0);
|
||||||
}
|
}
|
||||||
}, [backdrop, customBackdropUrl]);
|
}, [backdrop]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const logoUrl = (metadata && (metadata as any).logo) as string | undefined;
|
const logoUrl = (metadata && (metadata as any).logo) as string | undefined;
|
||||||
|
|
@ -3135,7 +3121,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
|
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
|
||||||
>
|
>
|
||||||
{(customBackdropUrl || backdrop) && (
|
{backdrop && (
|
||||||
<Animated.View style={[
|
<Animated.View style={[
|
||||||
StyleSheet.absoluteFill,
|
StyleSheet.absoluteFill,
|
||||||
{
|
{
|
||||||
|
|
@ -3145,7 +3131,7 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: customBackdropUrl || backdrop }}
|
source={{ uri: backdrop }}
|
||||||
style={StyleSheet.absoluteFillObject}
|
style={StyleSheet.absoluteFillObject}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import { storageService } from '../../services/storageService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { getSelectedBackdropUrl } from '../../utils/backdropStorage';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import Slider from '@react-native-community/slider';
|
import Slider from '@react-native-community/slider';
|
||||||
import KSPlayerComponent, { KSPlayerRef, KSPlayerSource } from './KSPlayerComponent';
|
import KSPlayerComponent, { KSPlayerRef, KSPlayerSource } from './KSPlayerComponent';
|
||||||
|
|
@ -136,9 +135,6 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const [isBackdropLoaded, setIsBackdropLoaded] = useState(false);
|
const [isBackdropLoaded, setIsBackdropLoaded] = useState(false);
|
||||||
const backdropImageOpacityAnim = useRef(new Animated.Value(0)).current;
|
const backdropImageOpacityAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
// Custom backdrop state
|
|
||||||
const [customBackdropUrl, setCustomBackdropUrl] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [isBuffering, setIsBuffering] = useState(false);
|
const [isBuffering, setIsBuffering] = useState(false);
|
||||||
const [ksAudioTracks, setKsAudioTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
|
const [ksAudioTracks, setKsAudioTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
|
||||||
const [ksTextTracks, setKsTextTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
|
const [ksTextTracks, setKsTextTracks] = useState<Array<{ id: number, name: string, language?: string }>>([]);
|
||||||
|
|
@ -316,26 +312,16 @@ const KSPlayerCore: React.FC = () => {
|
||||||
const hasLogo = metadata && metadata.logo && !metadataLoading;
|
const hasLogo = metadata && metadata.logo && !metadataLoading;
|
||||||
|
|
||||||
// Load custom backdrop on mount
|
// 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
|
// Prefetch backdrop and title logo for faster loading screen appearance
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const finalBackdrop = customBackdropUrl || backdrop;
|
if (backdrop && typeof backdrop === 'string') {
|
||||||
if (finalBackdrop && typeof finalBackdrop === 'string') {
|
|
||||||
// Reset loading state
|
// Reset loading state
|
||||||
setIsBackdropLoaded(false);
|
setIsBackdropLoaded(false);
|
||||||
backdropImageOpacityAnim.setValue(0);
|
backdropImageOpacityAnim.setValue(0);
|
||||||
|
|
||||||
// Prefetch the image
|
// Prefetch the image
|
||||||
try {
|
try {
|
||||||
FastImage.preload([{ uri: finalBackdrop }]);
|
FastImage.preload([{ uri: backdrop }]);
|
||||||
// Image prefetch initiated, fade it in smoothly
|
// Image prefetch initiated, fade it in smoothly
|
||||||
setIsBackdropLoaded(true);
|
setIsBackdropLoaded(true);
|
||||||
Animated.timing(backdropImageOpacityAnim, {
|
Animated.timing(backdropImageOpacityAnim, {
|
||||||
|
|
@ -354,7 +340,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
setIsBackdropLoaded(true);
|
setIsBackdropLoaded(true);
|
||||||
backdropImageOpacityAnim.setValue(0);
|
backdropImageOpacityAnim.setValue(0);
|
||||||
}
|
}
|
||||||
}, [backdrop, customBackdropUrl]);
|
}, [backdrop]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const logoUrl = (metadata && (metadata as any).logo) as string | undefined;
|
const logoUrl = (metadata && (metadata as any).logo) as string | undefined;
|
||||||
|
|
@ -2455,7 +2441,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
pointerEvents={shouldHideOpeningOverlay ? 'none' : 'auto'}
|
pointerEvents={shouldHideOpeningOverlay ? 'none' : 'auto'}
|
||||||
>
|
>
|
||||||
{(customBackdropUrl || backdrop) && (
|
{backdrop && (
|
||||||
<Animated.View style={[
|
<Animated.View style={[
|
||||||
StyleSheet.absoluteFill,
|
StyleSheet.absoluteFill,
|
||||||
{
|
{
|
||||||
|
|
@ -2465,7 +2451,7 @@ const KSPlayerCore: React.FC = () => {
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri: customBackdropUrl || backdrop }}
|
source={{ uri: backdrop }}
|
||||||
style={StyleSheet.absoluteFillObject}
|
style={StyleSheet.absoluteFillObject}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -786,30 +786,68 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
try {
|
try {
|
||||||
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) {
|
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) {
|
||||||
const tmdbSvc = TMDBService.getInstance();
|
const tmdbSvc = TMDBService.getInstance();
|
||||||
// Ensure we have a TMDB ID
|
|
||||||
let finalTmdbId: number | null = tmdbId;
|
let finalTmdbId: number | null = tmdbId;
|
||||||
if (!finalTmdbId) {
|
if (!finalTmdbId) {
|
||||||
finalTmdbId = await tmdbSvc.extractTMDBIdFromStremioId(actualId);
|
finalTmdbId = await tmdbSvc.extractTMDBIdFromStremioId(actualId);
|
||||||
if (finalTmdbId) setTmdbId(finalTmdbId);
|
if (finalTmdbId) setTmdbId(finalTmdbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalTmdbId) {
|
if (finalTmdbId) {
|
||||||
const lang = settings.tmdbLanguagePreference || 'en';
|
const lang = settings.tmdbLanguagePreference || 'en';
|
||||||
if (type === 'movie') {
|
if (type === 'movie') {
|
||||||
const localized = await tmdbSvc.getMovieDetails(String(finalTmdbId), lang);
|
const localized = await tmdbSvc.getMovieDetails(String(finalTmdbId), lang);
|
||||||
if (localized) {
|
if (localized) {
|
||||||
|
const movieDetailsObj = {
|
||||||
|
status: localized.status,
|
||||||
|
releaseDate: localized.release_date,
|
||||||
|
runtime: localized.runtime,
|
||||||
|
budget: localized.budget,
|
||||||
|
revenue: localized.revenue,
|
||||||
|
originalLanguage: localized.original_language,
|
||||||
|
originCountry: localized.production_countries?.map((c: any) => c.iso_3166_1),
|
||||||
|
tagline: localized.tagline,
|
||||||
|
};
|
||||||
|
const productionInfo = Array.isArray(localized.production_companies)
|
||||||
|
? localized.production_companies
|
||||||
|
.map((c: any) => ({ id: c?.id, name: c?.name, logo: tmdbSvc.getImageUrl(c?.logo_path, 'w185') }))
|
||||||
|
.filter((c: any) => c && (c.logo || c.name))
|
||||||
|
: [];
|
||||||
|
|
||||||
finalMetadata = {
|
finalMetadata = {
|
||||||
...finalMetadata,
|
...finalMetadata,
|
||||||
name: localized.title || finalMetadata.name,
|
name: localized.title || finalMetadata.name,
|
||||||
description: localized.overview || finalMetadata.description,
|
description: localized.overview || finalMetadata.description,
|
||||||
|
movieDetails: movieDetailsObj,
|
||||||
|
...(productionInfo.length > 0 && { networks: productionInfo }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else { // 'series'
|
||||||
const localized = await tmdbSvc.getTVShowDetails(Number(finalTmdbId), lang);
|
const localized = await tmdbSvc.getTVShowDetails(Number(finalTmdbId), lang);
|
||||||
if (localized) {
|
if (localized) {
|
||||||
|
const tvDetails = {
|
||||||
|
status: localized.status,
|
||||||
|
firstAirDate: localized.first_air_date,
|
||||||
|
lastAirDate: localized.last_air_date,
|
||||||
|
numberOfSeasons: localized.number_of_seasons,
|
||||||
|
numberOfEpisodes: localized.number_of_episodes,
|
||||||
|
episodeRunTime: localized.episode_run_time,
|
||||||
|
type: localized.type,
|
||||||
|
originCountry: localized.origin_country,
|
||||||
|
originalLanguage: localized.original_language,
|
||||||
|
createdBy: localized.created_by,
|
||||||
|
};
|
||||||
|
const productionInfo = Array.isArray(localized.networks)
|
||||||
|
? localized.networks
|
||||||
|
.map((n: any) => ({ id: n?.id, name: n?.name, logo: tmdbSvc.getImageUrl(n?.logo_path, 'w185') }))
|
||||||
|
.filter((n: any) => n && (n.logo || n.name))
|
||||||
|
: [];
|
||||||
|
|
||||||
finalMetadata = {
|
finalMetadata = {
|
||||||
...finalMetadata,
|
...finalMetadata,
|
||||||
name: localized.name || finalMetadata.name,
|
name: localized.name || finalMetadata.name,
|
||||||
description: localized.overview || finalMetadata.description,
|
description: localized.overview || finalMetadata.description,
|
||||||
|
tvDetails,
|
||||||
|
...(productionInfo.length > 0 && { networks: productionInfo }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1781,10 +1819,24 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
const tmdbService = TMDBService.getInstance();
|
const tmdbService = TMDBService.getInstance();
|
||||||
let productionInfo: any[] = [];
|
let productionInfo: any[] = [];
|
||||||
|
|
||||||
|
if (__DEV__) console.log('[useMetadata] fetchProductionInfo starting', {
|
||||||
|
contentKey,
|
||||||
|
type,
|
||||||
|
tmdbId,
|
||||||
|
useLocalized: settings.useTmdbLocalizedMetadata,
|
||||||
|
lang: settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en',
|
||||||
|
hasExistingNetworks: !!(metadata as any).networks
|
||||||
|
});
|
||||||
|
|
||||||
if (type === 'series') {
|
if (type === 'series') {
|
||||||
// Fetch networks and additional details for TV shows
|
// Fetch networks and additional details for TV shows
|
||||||
const showDetails = await tmdbService.getTVShowDetails(tmdbId, 'en-US');
|
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
||||||
|
const showDetails = await tmdbService.getTVShowDetails(tmdbId, lang);
|
||||||
if (showDetails) {
|
if (showDetails) {
|
||||||
|
if (__DEV__) console.log('[useMetadata] fetchProductionInfo got showDetails', {
|
||||||
|
hasNetworks: !!showDetails.networks,
|
||||||
|
networksCount: showDetails.networks?.length || 0
|
||||||
|
});
|
||||||
// Fetch networks
|
// Fetch networks
|
||||||
if (showDetails.networks) {
|
if (showDetails.networks) {
|
||||||
productionInfo = Array.isArray(showDetails.networks)
|
productionInfo = Array.isArray(showDetails.networks)
|
||||||
|
|
@ -1821,8 +1873,13 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
}
|
}
|
||||||
} else if (type === 'movie') {
|
} else if (type === 'movie') {
|
||||||
// Fetch production companies and additional details for movies
|
// Fetch production companies and additional details for movies
|
||||||
const movieDetails = await tmdbService.getMovieDetails(String(tmdbId), 'en-US');
|
const lang = settings.useTmdbLocalizedMetadata ? (settings.tmdbLanguagePreference || 'en') : 'en';
|
||||||
|
const movieDetails = await tmdbService.getMovieDetails(String(tmdbId), lang);
|
||||||
if (movieDetails) {
|
if (movieDetails) {
|
||||||
|
if (__DEV__) console.log('[useMetadata] fetchProductionInfo got movieDetails', {
|
||||||
|
hasProductionCompanies: !!movieDetails.production_companies,
|
||||||
|
productionCompaniesCount: movieDetails.production_companies?.length || 0
|
||||||
|
});
|
||||||
// Fetch production companies
|
// Fetch production companies
|
||||||
if (movieDetails.production_companies) {
|
if (movieDetails.production_companies) {
|
||||||
productionInfo = Array.isArray(movieDetails.production_companies)
|
productionInfo = Array.isArray(movieDetails.production_companies)
|
||||||
|
|
@ -1844,7 +1901,7 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
budget: movieDetails.budget,
|
budget: movieDetails.budget,
|
||||||
revenue: movieDetails.revenue,
|
revenue: movieDetails.revenue,
|
||||||
originalLanguage: movieDetails.original_language,
|
originalLanguage: movieDetails.original_language,
|
||||||
originCountry: movieDetails.origin_country,
|
originCountry: movieDetails.production_countries?.map((c: any) => c.iso_3166_1),
|
||||||
tagline: movieDetails.tagline,
|
tagline: movieDetails.tagline,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1859,7 +1916,10 @@ export const useMetadata = ({ id, type, addonId }: UseMetadataProps): UseMetadat
|
||||||
|
|
||||||
if (__DEV__) console.log('[useMetadata] Fetched production info via TMDB:', productionInfo);
|
if (__DEV__) console.log('[useMetadata] Fetched production info via TMDB:', productionInfo);
|
||||||
if (productionInfo.length > 0) {
|
if (productionInfo.length > 0) {
|
||||||
|
if (__DEV__) console.log('[useMetadata] Setting production info on metadata', { productionInfoCount: productionInfo.length });
|
||||||
setMetadata((prev: any) => ({ ...prev, networks: productionInfo }));
|
setMetadata((prev: any) => ({ ...prev, networks: productionInfo }));
|
||||||
|
} else {
|
||||||
|
if (__DEV__) console.log('[useMetadata] No production info found, not setting networks');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (__DEV__) console.error('[useMetadata] Failed to fetch production info:', error);
|
if (__DEV__) console.error('[useMetadata] Failed to fetch production info:', error);
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,11 @@ import {
|
||||||
Dimensions,
|
Dimensions,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Alert,
|
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
||||||
import { TMDBService } from '../services/tmdbService';
|
import { TMDBService } from '../services/tmdbService';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
|
|
@ -22,7 +20,6 @@ const { width } = Dimensions.get('window');
|
||||||
const BACKDROP_WIDTH = width * 0.9;
|
const BACKDROP_WIDTH = width * 0.9;
|
||||||
const BACKDROP_HEIGHT = (BACKDROP_WIDTH * 9) / 16; // 16:9 aspect ratio
|
const BACKDROP_HEIGHT = (BACKDROP_WIDTH * 9) / 16; // 16:9 aspect ratio
|
||||||
|
|
||||||
const SELECTED_BACKDROP_KEY = 'selected_custom_backdrop';
|
|
||||||
|
|
||||||
interface BackdropItem {
|
interface BackdropItem {
|
||||||
file_path: string;
|
file_path: string;
|
||||||
|
|
@ -46,7 +43,6 @@ const BackdropGalleryScreen: React.FC = () => {
|
||||||
const [backdrops, setBackdrops] = useState<BackdropItem[]>([]);
|
const [backdrops, setBackdrops] = useState<BackdropItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [selectedBackdrop, setSelectedBackdrop] = useState<BackdropItem | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchBackdrops = async () => {
|
const fetchBackdrops = async () => {
|
||||||
|
|
@ -89,78 +85,18 @@ const BackdropGalleryScreen: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [tmdbId, type]);
|
}, [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!', [
|
|
||||||
{ text: 'OK', onPress: () => navigation.goBack() }
|
|
||||||
]);
|
|
||||||
} 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!', [
|
|
||||||
{ text: 'OK', onPress: () => navigation.goBack() }
|
|
||||||
]);
|
|
||||||
} 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 renderBackdrop = ({ item, index }: { item: BackdropItem; index: number }) => {
|
||||||
const imageUrl = `https://image.tmdb.org/t/p/w1280${item.file_path}`;
|
const imageUrl = `https://image.tmdb.org/t/p/w1280${item.file_path}`;
|
||||||
const isSelected = selectedBackdrop?.file_path === item.file_path;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<View style={styles.backdropContainer}>
|
||||||
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
|
<FastImage
|
||||||
source={{ uri: imageUrl }}
|
source={{ uri: imageUrl }}
|
||||||
style={styles.backdropImage}
|
style={styles.backdropImage}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
/>
|
/>
|
||||||
{isSelected && (
|
|
||||||
<View style={styles.selectedIndicator}>
|
|
||||||
<MaterialIcons name="check-circle" size={24} color={currentTheme.colors.highEmphasis} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={styles.backdropInfo}>
|
<View style={styles.backdropInfo}>
|
||||||
<Text style={[styles.backdropResolution, { color: currentTheme.colors.highEmphasis }]}>
|
<Text style={[styles.backdropResolution, { color: currentTheme.colors.highEmphasis }]}>
|
||||||
{item.width} × {item.height}
|
{item.width} × {item.height}
|
||||||
|
|
@ -169,7 +105,7 @@ const BackdropGalleryScreen: React.FC = () => {
|
||||||
{item.aspect_ratio.toFixed(2)}:1
|
{item.aspect_ratio.toFixed(2)}:1
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -189,23 +125,6 @@ const BackdropGalleryScreen: React.FC = () => {
|
||||||
{backdrops.length} Backdrop{backdrops.length !== 1 ? 's' : ''}
|
{backdrops.length} Backdrop{backdrops.length !== 1 ? 's' : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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={currentTheme.colors.highEmphasis} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -242,13 +161,6 @@ const BackdropGalleryScreen: React.FC = () => {
|
||||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
|
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" />
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
|
|
||||||
{/* Explanatory note */}
|
|
||||||
<View style={styles.noteContainer}>
|
|
||||||
<Text style={[styles.noteText, { color: currentTheme.colors.textMuted }]}>
|
|
||||||
Long press any backdrop to set it as your default for metadata screens and player loading overlay.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={backdrops}
|
data={backdrops}
|
||||||
keyExtractor={(item, index) => `${item.file_path}-${index}`}
|
keyExtractor={(item, index) => `${item.file_path}-${index}`}
|
||||||
|
|
@ -317,31 +229,6 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
opacity: 0.8,
|
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,
|
|
||||||
textAlign: 'center',
|
|
||||||
lineHeight: 16,
|
|
||||||
},
|
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ import { useSettings } from '../hooks/useSettings';
|
||||||
import { MetadataLoadingScreen } from '../components/loading/MetadataLoadingScreen';
|
import { MetadataLoadingScreen } from '../components/loading/MetadataLoadingScreen';
|
||||||
import { useTrailer } from '../contexts/TrailerContext';
|
import { useTrailer } from '../contexts/TrailerContext';
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
import FastImage from '@d11/react-native-fast-image';
|
||||||
import { getSelectedBackdropUrl } from '../utils/backdropStorage';
|
|
||||||
|
|
||||||
// Import our optimized components and hooks
|
// Import our optimized components and hooks
|
||||||
import HeroSection from '../components/metadata/HeroSection';
|
import HeroSection from '../components/metadata/HeroSection';
|
||||||
|
|
@ -96,31 +95,21 @@ const MetadataScreen: React.FC = () => {
|
||||||
const transitionOpacity = useSharedValue(1);
|
const transitionOpacity = useSharedValue(1);
|
||||||
const interactionComplete = useRef(false);
|
const interactionComplete = useRef(false);
|
||||||
|
|
||||||
|
// Animation values for network/production sections
|
||||||
|
const networkSectionOpacity = useSharedValue(0);
|
||||||
|
const productionSectionOpacity = useSharedValue(0);
|
||||||
|
|
||||||
// Comment bottom sheet state
|
// Comment bottom sheet state
|
||||||
const [commentBottomSheetVisible, setCommentBottomSheetVisible] = useState(false);
|
const [commentBottomSheetVisible, setCommentBottomSheetVisible] = useState(false);
|
||||||
const [selectedComment, setSelectedComment] = useState<any>(null);
|
const [selectedComment, setSelectedComment] = useState<any>(null);
|
||||||
const [revealedSpoilers, setRevealedSpoilers] = useState<Set<string>>(new Set());
|
const [revealedSpoilers, setRevealedSpoilers] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Custom backdrop state
|
|
||||||
const [customBackdropUrl, setCustomBackdropUrl] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Debug state changes
|
// Debug state changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('MetadataScreen: commentBottomSheetVisible changed to:', commentBottomSheetVisible);
|
console.log('MetadataScreen: commentBottomSheetVisible changed to:', commentBottomSheetVisible);
|
||||||
}, [commentBottomSheetVisible]);
|
}, [commentBottomSheetVisible]);
|
||||||
|
|
||||||
// Load custom backdrop when screen comes into focus (for instant updates when returning from gallery)
|
|
||||||
useFocusEffect(
|
|
||||||
React.useCallback(() => {
|
|
||||||
const loadCustomBackdrop = async () => {
|
|
||||||
const backdropUrl = await getSelectedBackdropUrl('original');
|
|
||||||
setCustomBackdropUrl(backdropUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
loadCustomBackdrop();
|
|
||||||
}, [])
|
|
||||||
);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('MetadataScreen: selectedComment changed to:', selectedComment?.id);
|
console.log('MetadataScreen: selectedComment changed to:', selectedComment?.id);
|
||||||
}, [selectedComment]);
|
}, [selectedComment]);
|
||||||
|
|
@ -146,6 +135,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
tmdbId,
|
tmdbId,
|
||||||
} = useMetadata({ id, type, addonId });
|
} = useMetadata({ id, type, addonId });
|
||||||
|
|
||||||
|
|
||||||
// Log useMetadata hook state changes for debugging
|
// Log useMetadata hook state changes for debugging
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log('🔍 [MetadataScreen] useMetadata state:', {
|
console.log('🔍 [MetadataScreen] useMetadata state:', {
|
||||||
|
|
@ -164,6 +154,28 @@ const MetadataScreen: React.FC = () => {
|
||||||
});
|
});
|
||||||
}, [loading, metadata, metadataError, cast.length, episodes.length, Object.keys(groupedEpisodes).length, imdbId, tmdbId]);
|
}, [loading, metadata, metadataError, cast.length, episodes.length, Object.keys(groupedEpisodes).length, imdbId, tmdbId]);
|
||||||
|
|
||||||
|
// Animate network section when data becomes available (for series)
|
||||||
|
useEffect(() => {
|
||||||
|
const hasNetworks = metadata?.networks && metadata.networks.length > 0;
|
||||||
|
const isSeries = Object.keys(groupedEpisodes).length > 0;
|
||||||
|
const shouldShow = shouldLoadSecondaryData && hasNetworks && isSeries;
|
||||||
|
|
||||||
|
if (shouldShow && networkSectionOpacity.value === 0) {
|
||||||
|
networkSectionOpacity.value = withTiming(1, { duration: 400 });
|
||||||
|
}
|
||||||
|
}, [metadata?.networks, Object.keys(groupedEpisodes).length, shouldLoadSecondaryData, networkSectionOpacity]);
|
||||||
|
|
||||||
|
// Animate production section when data becomes available (for movies)
|
||||||
|
useEffect(() => {
|
||||||
|
const hasNetworks = metadata?.networks && metadata.networks.length > 0;
|
||||||
|
const isMovie = Object.keys(groupedEpisodes).length === 0;
|
||||||
|
const shouldShow = shouldLoadSecondaryData && hasNetworks && isMovie;
|
||||||
|
|
||||||
|
if (shouldShow && productionSectionOpacity.value === 0) {
|
||||||
|
productionSectionOpacity.value = withTiming(1, { duration: 400 });
|
||||||
|
}
|
||||||
|
}, [metadata?.networks, Object.keys(groupedEpisodes).length, shouldLoadSecondaryData, productionSectionOpacity]);
|
||||||
|
|
||||||
// Optimized hooks with memoization and conditional loading
|
// Optimized hooks with memoization and conditional loading
|
||||||
const watchProgressData = useWatchProgress(id, Object.keys(groupedEpisodes).length > 0 ? 'series' : type as 'movie' | 'series', episodeId, episodes);
|
const watchProgressData = useWatchProgress(id, Object.keys(groupedEpisodes).length > 0 ? 'series' : type as 'movie' | 'series', episodeId, episodes);
|
||||||
const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
|
const assetData = useMetadataAssets(metadata, id, type, imdbId, settings, setMetadata);
|
||||||
|
|
@ -241,6 +253,15 @@ const MetadataScreen: React.FC = () => {
|
||||||
return { backgroundColor: color as any };
|
return { backgroundColor: color as any };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Animated styles for network and production sections
|
||||||
|
const networkSectionAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
|
opacity: networkSectionOpacity.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const productionSectionAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
|
opacity: productionSectionOpacity.value,
|
||||||
|
}));
|
||||||
|
|
||||||
// For compatibility with existing code, maintain the static value as well
|
// For compatibility with existing code, maintain the static value as well
|
||||||
const dynamicBackgroundColor = useMemo(() => {
|
const dynamicBackgroundColor = useMemo(() => {
|
||||||
if (settings.useDominantBackgroundColor && dominantColor && dominantColor !== '#1a1a1a' && dominantColor !== null && dominantColor !== currentTheme.colors.darkBackground) {
|
if (settings.useDominantBackgroundColor && dominantColor && dominantColor !== '#1a1a1a' && dominantColor !== null && dominantColor !== currentTheme.colors.darkBackground) {
|
||||||
|
|
@ -830,7 +851,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
{/* Hero Section - Optimized */}
|
{/* Hero Section - Optimized */}
|
||||||
<HeroSection
|
<HeroSection
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
bannerImage={customBackdropUrl || assetData.bannerImage}
|
bannerImage={assetData.bannerImage}
|
||||||
loadingBanner={assetData.loadingBanner}
|
loadingBanner={assetData.loadingBanner}
|
||||||
logoLoadError={assetData.logoLoadError}
|
logoLoadError={assetData.logoLoadError}
|
||||||
scrollY={animations.scrollY}
|
scrollY={animations.scrollY}
|
||||||
|
|
@ -873,7 +894,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Production info row — shown below description and above cast for series */}
|
{/* Production info row — shown below description and above cast for series */}
|
||||||
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.networks && metadata.networks.length > 0 && (
|
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.networks && metadata.networks.length > 0 && (
|
||||||
<View style={styles.productionContainer}>
|
<Animated.View style={[styles.productionContainer, networkSectionAnimatedStyle]}>
|
||||||
<Text style={styles.productionHeader}>Network</Text>
|
<Text style={styles.productionHeader}>Network</Text>
|
||||||
<View style={styles.productionRow}>
|
<View style={styles.productionRow}>
|
||||||
{metadata.networks.slice(0, 6).map((net) => (
|
{metadata.networks.slice(0, 6).map((net) => (
|
||||||
|
|
@ -890,7 +911,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Cast Section with skeleton when loading - Lazy loaded */}
|
{/* Cast Section with skeleton when loading - Lazy loaded */}
|
||||||
|
|
@ -905,7 +926,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
|
|
||||||
{/* Production info row — shown after cast for movies */}
|
{/* Production info row — shown after cast for movies */}
|
||||||
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length === 0 && metadata?.networks && metadata.networks.length > 0 && (
|
{shouldLoadSecondaryData && Object.keys(groupedEpisodes).length === 0 && metadata?.networks && metadata.networks.length > 0 && (
|
||||||
<View style={styles.productionContainer}>
|
<Animated.View style={[styles.productionContainer, productionSectionAnimatedStyle]}>
|
||||||
<Text style={styles.productionHeader}>Production</Text>
|
<Text style={styles.productionHeader}>Production</Text>
|
||||||
<View style={styles.productionRow}>
|
<View style={styles.productionRow}>
|
||||||
{metadata.networks.slice(0, 6).map((net) => (
|
{metadata.networks.slice(0, 6).map((net) => (
|
||||||
|
|
@ -922,7 +943,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comments Section - Lazy loaded */}
|
{/* Comments Section - Lazy loaded */}
|
||||||
|
|
|
||||||
|
|
@ -1160,7 +1160,7 @@ export const StreamsScreen = () => {
|
||||||
episodeId: (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined,
|
episodeId: (type === 'series' || type === 'other') && selectedEpisode ? selectedEpisode : undefined,
|
||||||
imdbId: imdbId || undefined,
|
imdbId: imdbId || undefined,
|
||||||
availableStreams: streamsToPass,
|
availableStreams: streamsToPass,
|
||||||
backdrop: bannerImage || undefined,
|
backdrop: bannerImage,
|
||||||
// Hint for Android ExoPlayer/react-native-video
|
// Hint for Android ExoPlayer/react-native-video
|
||||||
videoType: videoType,
|
videoType: videoType,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
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