import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, useColorScheme, StatusBar, ImageBackground, Dimensions, Platform, TouchableWithoutFeedback, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { Image } from 'expo-image'; import { colors } from '../styles/colors'; import { useMetadata } from '../hooks/useMetadata'; import { CastSection as OriginalCastSection } from '../components/metadata/CastSection'; import { SeriesContent as OriginalSeriesContent } from '../components/metadata/SeriesContent'; import { MovieContent as OriginalMovieContent } from '../components/metadata/MovieContent'; import { MoreLikeThisSection as OriginalMoreLikeThisSection } from '../components/metadata/MoreLikeThisSection'; import { RatingsSection as OriginalRatingsSection } from '../components/metadata/RatingsSection'; import { StreamingContent } from '../services/catalogService'; import { GroupedStreams } from '../types/streams'; import { TMDBEpisode } from '../services/tmdbService'; import { Cast } from '../types/cast'; import { RouteParams, Episode } from '../types/metadata'; import Animated, { useAnimatedStyle, withTiming, useSharedValue, Easing, FadeInDown, interpolate, Extrapolate, withSpring, FadeIn, runOnJS, Layout, } from 'react-native-reanimated'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; import { TMDBService } from '../services/tmdbService'; import { storageService } from '../services/storageService'; import { logger } from '../utils/logger'; import { useGenres } from '../contexts/GenreContext'; const { width, height } = Dimensions.get('window'); // Memoize child components const CastSection = React.memo(OriginalCastSection); const SeriesContent = React.memo(OriginalSeriesContent); const MovieContent = React.memo(OriginalMovieContent); const MoreLikeThisSection = React.memo(OriginalMoreLikeThisSection); const RatingsSection = React.memo(OriginalRatingsSection); // Animation constants const springConfig = { damping: 20, mass: 1, stiffness: 100 }; // Animation timing constants for staggered appearance const ANIMATION_DELAY_CONSTANTS = { HERO: 100, LOGO: 250, PROGRESS: 350, GENRES: 400, BUTTONS: 450, CONTENT: 500 }; // Add debug log for storageService logger.log('[MetadataScreen] StorageService instance:', storageService); // Memoized ActionButtons Component const ActionButtons = React.memo(({ handleShowStreams, toggleLibrary, inLibrary, type, id, navigation, playButtonText, animatedStyle }: { handleShowStreams: () => void; toggleLibrary: () => void; inLibrary: boolean; type: 'movie' | 'series'; id: string; navigation: NavigationProp; playButtonText: string; animatedStyle: any; }) => ( {playButtonText} {inLibrary ? 'Saved' : 'Save'} {type === 'series' && ( { const tmdb = TMDBService.getInstance(); const tmdbId = await tmdb.extractTMDBIdFromStremioId(id); if (tmdbId) { navigation.navigate('ShowRatings', { showId: tmdbId }); } else { logger.error('Could not find TMDB ID for show'); } }} > )} )); // Memoized WatchProgress Component const WatchProgressDisplay = React.memo(({ watchProgress, type, getEpisodeDetails, animatedStyle }: { watchProgress: { currentTime: number; duration: number; lastUpdated: number; episodeId?: string } | null; type: 'movie' | 'series'; getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null; animatedStyle: any; }) => { if (!watchProgress || watchProgress.duration === 0) { return null; } const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString(); let episodeInfo = ''; if (type === 'series' && watchProgress.episodeId) { const details = getEpisodeDetails(watchProgress.episodeId); if (details) { episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`; } } return ( {progressPercent >= 95 ? 'Watched' : `${Math.round(progressPercent)}% watched`}{episodeInfo} • Last watched on {formattedTime} ); }); const MetadataScreen = () => { const route = useRoute, string>>(); const navigation = useNavigation>(); const { id, type, episodeId } = route.params; const { metadata, loading, error: metadataError, cast, loadingCast, episodes, selectedSeason, loadingSeasons, loadMetadata, handleSeasonChange, toggleLibrary, inLibrary, groupedEpisodes, recommendations, loadingRecommendations, setMetadata, } = useMetadata({ id, type }); // Get genres from context const { genreMap, loadingGenres } = useGenres(); const contentRef = useRef(null); const [lastScrollTop, setLastScrollTop] = useState(0); const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false); // Animation values const screenScale = useSharedValue(0.92); const screenOpacity = useSharedValue(0); const heroHeight = useSharedValue(height * 0.5); const contentTranslateY = useSharedValue(60); // Additional animation values for staggered entrance const heroScale = useSharedValue(1.05); const heroOpacity = useSharedValue(0); const genresOpacity = useSharedValue(0); const genresTranslateY = useSharedValue(20); const buttonsOpacity = useSharedValue(0); const buttonsTranslateY = useSharedValue(30); // Add state for watch progress const [watchProgress, setWatchProgress] = useState<{ currentTime: number; duration: number; lastUpdated: number; episodeId?: string; } | null>(null); // Add new animated value for watch progress const watchProgressOpacity = useSharedValue(0); const watchProgressScaleY = useSharedValue(0); // Add animated value for logo const logoOpacity = useSharedValue(0); const logoScale = useSharedValue(0.9); // Debug log for route params // logger.log('[MetadataScreen] Component mounted with route params:', { id, type, episodeId }); // Fetch logo immediately for TMDB content useEffect(() => { if (metadata && id.startsWith('tmdb:')) { const fetchLogo = async () => { try { const tmdbId = id.split(':')[1]; const tmdbType = type === 'series' ? 'tv' : 'movie'; const logoUrl = await TMDBService.getInstance().getContentLogo(tmdbType, tmdbId); if (logoUrl) { // Update metadata with logo setMetadata(prevMetadata => ({ ...prevMetadata!, logo: logoUrl })); logger.log(`Successfully fetched logo for ${type} ${tmdbId} from TMDB on MetadataScreen`); } } catch (error) { logger.error('Failed to fetch logo in MetadataScreen:', error); } }; fetchLogo(); } }, [id, type, metadata, setMetadata]); // Function to get episode details from episodeId const getEpisodeDetails = useCallback((episodeId: string): { seasonNumber: string; episodeNumber: string; episodeName: string } | null => { // Try to parse from format "seriesId:season:episode" const parts = episodeId.split(':'); if (parts.length === 3) { const [, seasonNum, episodeNum] = parts; // Find episode in our local episodes array const episode = episodes.find( ep => ep.season_number === parseInt(seasonNum) && ep.episode_number === parseInt(episodeNum) ); if (episode) { return { seasonNumber: seasonNum, episodeNumber: episodeNum, episodeName: episode.name }; } } // If not found by season/episode, try stremioId const episodeByStremioId = episodes.find(ep => ep.stremioId === episodeId); if (episodeByStremioId) { return { seasonNumber: episodeByStremioId.season_number.toString(), episodeNumber: episodeByStremioId.episode_number.toString(), episodeName: episodeByStremioId.name }; } return null; }, [episodes]); const loadWatchProgress = useCallback(async () => { try { if (id && type) { if (type === 'series') { const allProgress = await storageService.getAllWatchProgress(); // Function to get episode number from episodeId const getEpisodeNumber = (epId: string) => { const parts = epId.split(':'); if (parts.length === 3) { return { season: parseInt(parts[1]), episode: parseInt(parts[2]) }; } return null; }; // Get all episodes for this series with progress const seriesProgresses = Object.entries(allProgress) .filter(([key]) => key.includes(`${type}:${id}:`)) .map(([key, value]) => ({ episodeId: key.split(`${type}:${id}:`)[1], progress: value })) .filter(({ episodeId, progress }) => { const progressPercent = (progress.currentTime / progress.duration) * 100; return progressPercent > 0; }); // If we have a specific episodeId in route params if (episodeId) { const progress = await storageService.getWatchProgress(id, type, episodeId); if (progress) { const progressPercent = (progress.currentTime / progress.duration) * 100; // If current episode is finished (≥95%), try to find next unwatched episode if (progressPercent >= 95) { const currentEpNum = getEpisodeNumber(episodeId); if (currentEpNum && episodes.length > 0) { // Find the next episode const nextEpisode = episodes.find(ep => { // First check in same season if (ep.season_number === currentEpNum.season && ep.episode_number > currentEpNum.episode) { const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`; const epProgress = seriesProgresses.find(p => p.episodeId === epId); if (!epProgress) return true; const percent = (epProgress.progress.currentTime / epProgress.progress.duration) * 100; return percent < 95; } // Then check next seasons if (ep.season_number > currentEpNum.season) { const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`; const epProgress = seriesProgresses.find(p => p.episodeId === epId); if (!epProgress) return true; const percent = (epProgress.progress.currentTime / epProgress.progress.duration) * 100; return percent < 95; } return false; }); if (nextEpisode) { const nextEpisodeId = nextEpisode.stremioId || `${id}:${nextEpisode.season_number}:${nextEpisode.episode_number}`; const nextProgress = await storageService.getWatchProgress(id, type, nextEpisodeId); if (nextProgress) { setWatchProgress({ ...nextProgress, episodeId: nextEpisodeId }); } else { setWatchProgress({ currentTime: 0, duration: 0, lastUpdated: Date.now(), episodeId: nextEpisodeId }); } return; } } // If no next episode found or current episode is finished, show no progress setWatchProgress(null); return; } // If current episode is not finished, show its progress setWatchProgress({ ...progress, episodeId }); } else { setWatchProgress(null); } } else { // Find the first unfinished episode const unfinishedEpisode = episodes.find(ep => { const epId = ep.stremioId || `${id}:${ep.season_number}:${ep.episode_number}`; const progress = seriesProgresses.find(p => p.episodeId === epId); if (!progress) return true; const percent = (progress.progress.currentTime / progress.progress.duration) * 100; return percent < 95; }); if (unfinishedEpisode) { const epId = unfinishedEpisode.stremioId || `${id}:${unfinishedEpisode.season_number}:${unfinishedEpisode.episode_number}`; const progress = await storageService.getWatchProgress(id, type, epId); if (progress) { setWatchProgress({ ...progress, episodeId: epId }); } else { setWatchProgress({ currentTime: 0, duration: 0, lastUpdated: Date.now(), episodeId: epId }); } } else { setWatchProgress(null); } } } else { // For movies const progress = await storageService.getWatchProgress(id, type, episodeId); if (progress && progress.currentTime > 0) { const progressPercent = (progress.currentTime / progress.duration) * 100; if (progressPercent >= 95) { setWatchProgress(null); } else { setWatchProgress({ ...progress, episodeId }); } } else { setWatchProgress(null); } } } } catch (error) { logger.error('[MetadataScreen] Error loading watch progress:', error); setWatchProgress(null); } }, [id, type, episodeId, episodes, getEpisodeDetails]); // Initial load useEffect(() => { loadWatchProgress(); }, [loadWatchProgress]); // Refresh when screen comes into focus useFocusEffect( useCallback(() => { loadWatchProgress(); }, [loadWatchProgress]) ); // Function to get play button text const getPlayButtonText = useCallback(() => { if (!watchProgress || watchProgress.currentTime <= 0) { return 'Play'; } // Consider episode complete if progress is >= 95% const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100; if (progressPercent >= 95) { return 'Play'; } return 'Resume'; }, [watchProgress]); // Add effect to animate watch progress when it changes useEffect(() => { if (watchProgress && watchProgress.duration > 0) { watchProgressOpacity.value = withSpring(1, { mass: 0.2, stiffness: 100, damping: 14 }); watchProgressScaleY.value = withSpring(1, { mass: 0.3, stiffness: 120, damping: 18 }); } else { watchProgressOpacity.value = withSpring(0, { mass: 0.2, stiffness: 100, damping: 14 }); watchProgressScaleY.value = withSpring(0, { mass: 0.3, stiffness: 120, damping: 18 }); } }, [watchProgress, watchProgressOpacity, watchProgressScaleY]); // Add animated style for watch progress const watchProgressAnimatedStyle = useAnimatedStyle(() => { const translateY = interpolate( watchProgressScaleY.value, [0, 1], [-8, 0], Extrapolate.CLAMP ); return { opacity: watchProgressOpacity.value, transform: [ { translateY: translateY }, { scaleY: watchProgressScaleY.value } ] }; }); // Add animated style for logo const logoAnimatedStyle = useAnimatedStyle(() => { return { opacity: logoOpacity.value, transform: [{ scale: logoScale.value }] }; }); // Effect to animate logo when it's available useEffect(() => { if (metadata?.logo) { logoOpacity.value = withTiming(1, { duration: 500, easing: Easing.out(Easing.ease) }); } else { logoOpacity.value = withTiming(0, { duration: 200, easing: Easing.in(Easing.ease) }); } }, [metadata?.logo, logoOpacity]); // Update the watch progress render function - Now uses WatchProgressDisplay component // const renderWatchProgress = () => { ... }; // Removed old inline function // Handler functions const handleShowStreams = useCallback(() => { if (type === 'series') { // If we have watch progress with an episodeId, use that if (watchProgress?.episodeId) { navigation.navigate('Streams', { id, type, episodeId: watchProgress.episodeId }); return; } // If we have a specific episodeId from route params, use that if (episodeId) { navigation.navigate('Streams', { id, type, episodeId }); return; } // Otherwise, if we have episodes, start with the first one if (episodes.length > 0) { const firstEpisode = episodes[0]; const newEpisodeId = firstEpisode.stremioId || `${id}:${firstEpisode.season_number}:${firstEpisode.episode_number}`; navigation.navigate('Streams', { id, type, episodeId: newEpisodeId }); return; } } navigation.navigate('Streams', { id, type, episodeId }); }, [navigation, id, type, episodes, episodeId, watchProgress]); const handleSelectCastMember = useCallback((castMember: any) => { // Potentially navigate to a cast member screen or show details logger.log('Cast member selected:', castMember); }, []); // Empty dependency array as it doesn't depend on component state/props currently const handleEpisodeSelect = useCallback((episode: Episode) => { const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`; navigation.navigate('Streams', { id, type, episodeId }); }, [navigation, id, type]); // Added dependencies // Animated styles const containerAnimatedStyle = useAnimatedStyle(() => ({ flex: 1, transform: [{ scale: screenScale.value }], opacity: screenOpacity.value })); const heroAnimatedStyle = useAnimatedStyle(() => ({ width: '100%', height: heroHeight.value, backgroundColor: colors.black, transform: [{ scale: heroScale.value }], opacity: heroOpacity.value })); const contentAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: contentTranslateY.value }], opacity: interpolate( contentTranslateY.value, [60, 0], [0, 1], Extrapolate.CLAMP ) })); // Add animated style for genres const genresAnimatedStyle = useAnimatedStyle(() => { return { opacity: genresOpacity.value, transform: [{ translateY: genresTranslateY.value }] }; }); // Add animated style for buttons const buttonsAnimatedStyle = useAnimatedStyle(() => { return { opacity: buttonsOpacity.value, transform: [{ translateY: buttonsTranslateY.value }] }; }); // Debug logs for director/creator data React.useEffect(() => { if (metadata && metadata.id) { const fetchCrewData = async () => { try { const tmdb = TMDBService.getInstance(); const tmdbId = await tmdb.extractTMDBIdFromStremioId(id); if (tmdbId) { const credits = await tmdb.getCredits(tmdbId, type); // logger.log("Credits data structure:", JSON.stringify(credits).substring(0, 300)); // Extract directors for movies if (type === 'movie' && credits.crew) { const directors = credits.crew .filter((person: { job: string }) => person.job === 'Director') .map((director: { name: string }) => director.name); if (directors.length > 0 && metadata) { // Update metadata with directors setMetadata({ ...metadata, directors }); // logger.log("Updated directors:", directors); } } // Extract creators for TV shows if (type === 'series' && credits.crew) { const creators = credits.crew .filter((person: { job?: string; department?: string }) => person.job === 'Creator' || person.job === 'Series Creator' || person.department === 'Production' || person.job === 'Executive Producer' ) .map((creator: { name: string }) => creator.name); if (creators.length > 0 && metadata) { // Update metadata with creators setMetadata({ ...metadata, creators: creators.slice(0, 3) // Limit to first 3 creators }); // logger.log("Updated creators:", creators.slice(0, 3)); } } } } catch (error) { logger.error('Error fetching crew data:', error); } }; fetchCrewData(); } }, [metadata?.id, id, type, setMetadata]); // Start entrance animation React.useEffect(() => { // Use a timeout to ensure the animations starts after the component is mounted const animationTimeout = setTimeout(() => { // 1. First animate the container screenScale.value = withSpring(1, springConfig); screenOpacity.value = withSpring(1, springConfig); // 2. Then animate the hero section with a slight delay setTimeout(() => { heroOpacity.value = withSpring(1, { damping: 14, stiffness: 80 }); heroScale.value = withSpring(1, { damping: 18, stiffness: 100 }); }, ANIMATION_DELAY_CONSTANTS.HERO); // 3. Then animate the logo setTimeout(() => { logoOpacity.value = withSpring(1, { damping: 12, stiffness: 100 }); logoScale.value = withSpring(1, { damping: 14, stiffness: 90 }); }, ANIMATION_DELAY_CONSTANTS.LOGO); // 4. Then animate the watch progress if applicable setTimeout(() => { if (watchProgress && watchProgress.duration > 0) { watchProgressOpacity.value = withSpring(1, { damping: 14, stiffness: 100 }); watchProgressScaleY.value = withSpring(1, { damping: 18, stiffness: 120 }); } }, ANIMATION_DELAY_CONSTANTS.PROGRESS); // 5. Then animate the genres setTimeout(() => { genresOpacity.value = withSpring(1, { damping: 14, stiffness: 100 }); genresTranslateY.value = withSpring(0, { damping: 18, stiffness: 120 }); }, ANIMATION_DELAY_CONSTANTS.GENRES); // 6. Then animate the buttons setTimeout(() => { buttonsOpacity.value = withSpring(1, { damping: 14, stiffness: 100 }); buttonsTranslateY.value = withSpring(0, { damping: 18, stiffness: 120 }); }, ANIMATION_DELAY_CONSTANTS.BUTTONS); // 7. Finally animate the content section setTimeout(() => { contentTranslateY.value = withSpring(0, { damping: 25, mass: 1, stiffness: 100 }); }, ANIMATION_DELAY_CONSTANTS.CONTENT); }, 50); // Small timeout to ensure component is fully mounted return () => clearTimeout(animationTimeout); }, []); const handleBack = useCallback(() => { // Use goBack() which will return to the previous screen in the navigation stack // This will work for both cases: // 1. Coming from Calendar/ThisWeek - goes back to them // 2. Coming from StreamsScreen - goes back to Calendar/ThisWeek navigation.goBack(); }, [navigation]); // Function to render genres (updated to handle string array and use useMemo) const renderGenres = useMemo(() => { if (!metadata?.genres || !Array.isArray(metadata.genres) || metadata.genres.length === 0) { return null; } // Since metadata.genres is string[], we display them directly const genresToDisplay: string[] = metadata.genres as string[]; return genresToDisplay.slice(0, 4).map((genreName, index, array) => ( // Use React.Fragment to avoid extra View wrappers {genreName} {/* Add dot separator */} {index < array.length - 1 && ( )} )); }, [metadata?.genres]); // Dependency on metadata.genres if (loading) { return ( Loading content... ); } if (metadataError || !metadata) { return ( {metadataError || 'Content not found'} Try Again Go Back ); } return ( { // setLastScrollTop(e.nativeEvent.contentOffset.y); // Remove unused onScroll handler logic }} scrollEventThrottle={16} > {/* Hero Section */} {/* Title */} {metadata.logo ? ( ) : ( {metadata.name} )} {/* Watch Progress */} {/* Genre Tags */} {renderGenres} {/* Action Buttons */} {/* Main Content */} {/* Meta Info */} {metadata.year && ( {metadata.year} )} {metadata.runtime && ( {metadata.runtime} )} {metadata.certification && ( {metadata.certification} )} {metadata.imdbRating && ( {metadata.imdbRating} )} {/* Add RatingsSection right under the main metadata */} {id && ( )} {/* Creator/Director Info */} {metadata.directors && metadata.directors.length > 0 && ( Director{metadata.directors.length > 1 ? 's' : ''}: {metadata.directors.join(', ')} )} {metadata.creators && metadata.creators.length > 0 && ( Creator{metadata.creators.length > 1 ? 's' : ''}: {metadata.creators.join(', ')} )} {/* Description */} {metadata.description && ( setIsFullDescriptionOpen(!isFullDescriptionOpen)} activeOpacity={0.7} > {metadata.description} {isFullDescriptionOpen ? 'Show Less' : 'Show More'} )} {/* Cast Section */} {/* More Like This Section - Only for movies */} {type === 'movie' && ( )} {/* Type-specific content */} {type === 'series' ? ( ) : ( )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: 'transparent', paddingTop: 0, }, scrollView: { flex: 1, }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16, }, loadingText: { marginTop: 16, fontSize: 16, textAlign: 'center', }, errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32, }, errorText: { fontSize: 16, textAlign: 'center', marginTop: 16, marginBottom: 24, lineHeight: 24, }, retryButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 24, marginBottom: 16, }, retryButtonText: { color: colors.white, fontSize: 16, fontWeight: '600', }, backButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 24, borderWidth: 1, }, backButtonText: { fontSize: 16, fontWeight: '600', }, heroSection: { width: '100%', height: height * 0.5, backgroundColor: colors.black, overflow: 'hidden', }, heroImage: { width: '100%', height: '100%', top: '0%', transform: [{ scale: 1 }], }, heroGradient: { flex: 1, justifyContent: 'flex-end', paddingBottom: 24, }, heroContent: { padding: 16, paddingTop: 12, paddingBottom: 12, }, genreContainer: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', marginTop: 8, marginBottom: 16, gap: 4, }, genreText: { color: colors.text, fontSize: 12, fontWeight: '500', }, genreDot: { color: colors.text, fontSize: 12, fontWeight: '500', opacity: 0.6, marginHorizontal: 4, }, logoContainer: { alignItems: 'center', justifyContent: 'center', width: '100%', }, titleLogoContainer: { alignItems: 'center', justifyContent: 'center', width: '100%', }, titleLogo: { width: width * 0.65, height: 70, marginBottom: 0, alignSelf: 'center', }, heroTitle: { color: colors.highEmphasis, fontSize: 28, fontWeight: '900', marginBottom: 12, textShadowColor: 'rgba(0,0,0,0.75)', textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, letterSpacing: -0.5, }, metaInfo: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 16, marginBottom: 12, }, metaText: { color: colors.text, fontSize: 15, fontWeight: '700', letterSpacing: 0.3, textTransform: 'uppercase', opacity: 0.9, }, ratingContainer: { flexDirection: 'row', alignItems: 'center', }, imdbLogo: { width: 35, height: 18, marginRight: 4, }, ratingText: { color: colors.text, fontWeight: '700', fontSize: 15, letterSpacing: 0.3, }, descriptionContainer: { marginBottom: 16, paddingHorizontal: 16, }, description: { color: colors.mediumEmphasis, fontSize: 15, lineHeight: 24, }, showMoreButton: { flexDirection: 'row', alignItems: 'center', marginTop: 8, paddingVertical: 4, }, showMoreText: { color: colors.textMuted, fontSize: 14, marginRight: 4, }, actionButtons: { flexDirection: 'row', gap: 8, alignItems: 'center', marginBottom: -12, justifyContent: 'center', width: '100%', }, actionButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 10, borderRadius: 100, elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.3, shadowRadius: 4, flex: 1, }, playButton: { backgroundColor: colors.white, }, infoButton: { backgroundColor: 'rgba(255,255,255,0.2)', borderWidth: 2, borderColor: '#fff', }, iconButton: { width: 48, height: 48, borderRadius: 24, backgroundColor: 'rgba(255,255,255,0.2)', borderWidth: 2, borderColor: '#fff', alignItems: 'center', justifyContent: 'center', elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.3, shadowRadius: 4, }, playButtonText: { color: '#000', fontWeight: '600', marginLeft: 8, fontSize: 16, }, infoButtonText: { color: '#fff', marginLeft: 8, fontWeight: '600', fontSize: 16, }, creatorContainer: { marginBottom: 2, paddingHorizontal: 16, }, creatorSection: { flexDirection: 'row', alignItems: 'center', marginBottom: 4, height: 20 }, creatorLabel: { color: colors.white, fontSize: 14, fontWeight: '600', marginRight: 8, lineHeight: 20 }, creatorText: { color: colors.lightGray, fontSize: 14, flex: 1, lineHeight: 20 }, watchProgressContainer: { marginTop: 6, marginBottom: 8, width: '100%', alignItems: 'center', overflow: 'hidden', height: 48, }, watchProgressBar: { width: '75%', height: 3, backgroundColor: 'rgba(255, 255, 255, 0.15)', borderRadius: 1.5, overflow: 'hidden', marginBottom: 6 }, watchProgressFill: { height: '100%', backgroundColor: colors.primary, borderRadius: 1.5, }, watchProgressText: { color: colors.textMuted, fontSize: 12, textAlign: 'center', opacity: 0.9, letterSpacing: 0.2 }, }); export default MetadataScreen;