From 6d09add277363939b9ae7f99185a495b13db7a94 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sat, 30 Aug 2025 01:42:01 +0530 Subject: [PATCH] some ui changes to metadascreen --- src/components/metadata/SeriesContent.tsx | 438 ++++++++++++++++++---- src/screens/SettingsScreen.tsx | 68 ++++ 2 files changed, 432 insertions(+), 74 deletions(-) diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index 431c273..ff99882 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -10,9 +10,10 @@ import { Episode } from '../../types/metadata'; import { tmdbService } from '../../services/tmdbService'; import { storageService } from '../../services/storageService'; import { useFocusEffect } from '@react-navigation/native'; -import Animated, { FadeIn } from 'react-native-reanimated'; +import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft, withTiming, withSpring, useSharedValue, useAnimatedStyle, Easing } from 'react-native-reanimated'; import { TraktService } from '../../services/traktService'; import { logger } from '../../utils/logger'; +import AsyncStorage from '@react-native-async-storage/async-storage'; interface SeriesContentProps { episodes: Episode[]; @@ -49,9 +50,157 @@ export const SeriesContent: React.FC = ({ // Local TMDB hydration for rating/runtime when addon (Cinemeta) lacks these const [tmdbEpisodeOverrides, setTmdbEpisodeOverrides] = useState<{ [epKey: string]: { vote_average?: number; runtime?: number; still_path?: string } }>({}); + // Add state for season view mode (persists for current show across navigation) + const [seasonViewMode, setSeasonViewMode] = useState<'posters' | 'text'>('posters'); + + // Animated values for view mode transitions + const posterViewOpacity = useSharedValue(1); + const textViewOpacity = useSharedValue(0); + const posterViewTranslateX = useSharedValue(0); + const textViewTranslateX = useSharedValue(50); + const posterViewScale = useSharedValue(1); + const textViewScale = useSharedValue(0.95); + + // Animated styles for view transitions + const posterViewAnimatedStyle = useAnimatedStyle(() => ({ + opacity: posterViewOpacity.value, + transform: [ + { translateX: posterViewTranslateX.value }, + { scale: posterViewScale.value } + ], + })); + + const textViewAnimatedStyle = useAnimatedStyle(() => ({ + opacity: textViewOpacity.value, + transform: [ + { translateX: textViewTranslateX.value }, + { scale: textViewScale.value } + ], + })); + // Add refs for the scroll views const seasonScrollViewRef = useRef(null); const episodeScrollViewRef = useRef>(null); + const horizontalEpisodeScrollViewRef = useRef>(null); + + // Load saved view mode preference when component mounts or show changes + useEffect(() => { + const loadViewModePreference = async () => { + if (metadata?.id) { + try { + const savedMode = await AsyncStorage.getItem(`season_view_mode_${metadata.id}`); + if (savedMode === 'text' || savedMode === 'posters') { + setSeasonViewMode(savedMode); + console.log('[SeriesContent] Loaded saved view mode:', savedMode, 'for show:', metadata.id); + } + } catch (error) { + console.log('[SeriesContent] Error loading view mode preference:', error); + } + } + }; + + loadViewModePreference(); + }, [metadata?.id]); + + // Initialize animated values based on current view mode + useEffect(() => { + if (seasonViewMode === 'text') { + // Initialize text view as visible + posterViewOpacity.value = 0; + posterViewTranslateX.value = -60; + posterViewScale.value = 0.95; + textViewOpacity.value = 1; + textViewTranslateX.value = 0; + textViewScale.value = 1; + } else { + // Initialize poster view as visible + posterViewOpacity.value = 1; + posterViewTranslateX.value = 0; + posterViewScale.value = 1; + textViewOpacity.value = 0; + textViewTranslateX.value = 50; + textViewScale.value = 0.95; + } + }, [seasonViewMode]); + + // Save view mode preference when it changes + const updateViewMode = (newMode: 'posters' | 'text') => { + setSeasonViewMode(newMode); + if (metadata?.id) { + AsyncStorage.setItem(`season_view_mode_${metadata.id}`, newMode).catch(error => { + console.log('[SeriesContent] Error saving view mode preference:', error); + }); + } + }; + + // Animate view mode transition + const animateViewModeTransition = (newMode: 'posters' | 'text') => { + if (newMode === 'text') { + // Animate to text view with spring animations for smoother feel + posterViewOpacity.value = withTiming(0, { + duration: 250, + easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) + }); + posterViewTranslateX.value = withSpring(-60, { + damping: 20, + stiffness: 200, + mass: 0.8 + }); + posterViewScale.value = withSpring(0.95, { + damping: 20, + stiffness: 200, + mass: 0.8 + }); + + textViewOpacity.value = withTiming(1, { + duration: 300, + easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) + }); + textViewTranslateX.value = withSpring(0, { + damping: 20, + stiffness: 200, + mass: 0.8 + }); + textViewScale.value = withSpring(1, { + damping: 20, + stiffness: 200, + mass: 0.8 + }); + } else { + // Animate to poster view with spring animations + textViewOpacity.value = withTiming(0, { + duration: 250, + easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) + }); + textViewTranslateX.value = withSpring(60, { + damping: 20, + stiffness: 200, + mass: 0.8 + }); + textViewScale.value = withSpring(0.95, { + damping: 20, + stiffness: 200, + mass: 0.8 + }); + + posterViewOpacity.value = withTiming(1, { + duration: 300, + easing: Easing.bezier(0.25, 0.1, 0.25, 1.0) + }); + posterViewTranslateX.value = withSpring(0, { + damping: 20, + stiffness: 200, + mass: 0.8 + }); + posterViewScale.value = withSpring(1, { + damping: 20, + stiffness: 200, + mass: 0.8 + }); + } + }; + + // Add refs for the scroll views @@ -118,7 +267,7 @@ export const SeriesContent: React.FC = ({ // Function to find and scroll to the most recently watched episode const scrollToMostRecentEpisode = () => { - if (!metadata?.id || !episodeScrollViewRef.current || !settings?.episodeLayoutStyle || settings.episodeLayoutStyle !== 'horizontal') { + if (!metadata?.id || !settings?.episodeLayoutStyle || settings.episodeLayoutStyle !== 'horizontal') { return; } @@ -149,8 +298,8 @@ export const SeriesContent: React.FC = ({ const scrollPosition = mostRecentEpisodeIndex * cardWidth; setTimeout(() => { - if (episodeScrollViewRef.current && typeof (episodeScrollViewRef.current as any).scrollToOffset === 'function') { - (episodeScrollViewRef.current as any).scrollToOffset({ + if (horizontalEpisodeScrollViewRef.current) { + horizontalEpisodeScrollViewRef.current.scrollToOffset({ offset: scrollPosition, animated: true }); @@ -188,7 +337,7 @@ export const SeriesContent: React.FC = ({ // Fetch all episodes from TMDB and build override map for the current season const all = await tmdbService.getAllEpisodes(tmdbShowId); const overrides: { [k: string]: { vote_average?: number; runtime?: number; still_path?: string } } = {}; - const seasonEpisodes = all?.[String(selectedSeason)] || []; + const seasonEpisodes = all?.[selectedSeason] || []; seasonEpisodes.forEach((tmdbEp: any) => { const key = `${metadata.id}:${tmdbEp.season_number}:${tmdbEp.episode_number}`; overrides[key] = { @@ -275,15 +424,53 @@ export const SeriesContent: React.FC = ({ return null; } + console.log('[SeriesContent] renderSeasonSelector called, current view mode:', seasonViewMode); + const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b); return ( - Seasons + + Seasons + + {/* Dropdown Toggle Button */} + { + const newMode = seasonViewMode === 'posters' ? 'text' : 'posters'; + animateViewModeTransition(newMode); + updateViewMode(newMode); + console.log('[SeriesContent] View mode changed to:', newMode, 'Current ref value:', seasonViewMode); + }} + activeOpacity={0.7} + > + + {seasonViewMode === 'posters' ? 'Posters' : 'Text'} + + + + >} data={seasons} @@ -296,6 +483,8 @@ export const SeriesContent: React.FC = ({ windowSize={3} renderItem={({ item: season }) => { const seasonEpisodes = groupedEpisodes[season] || []; + + // Get season poster URL (needed for both views) let seasonPoster = DEFAULT_PLACEHOLDER; if (seasonEpisodes[0]?.season_poster_path) { const tmdbUrl = tmdbService.getImageUrl(seasonEpisodes[0].season_poster_path, 'w500'); @@ -304,51 +493,94 @@ export const SeriesContent: React.FC = ({ seasonPoster = metadata.poster; } - return ( - onSeasonChange(season)} - > - - - {selectedSeason === season && ( - - )} - {/* Show episode count badge, including when there are no episodes */} - - - {seasonEpisodes.length} ep{seasonEpisodes.length !== 1 ? 's' : ''} - - - - - Season {season} - - + onSeasonChange(season)} + > + + Season {season} + + + + ); + } + + // Poster view (current implementation) + console.log('[SeriesContent] Rendering poster view for season:', season, 'View mode ref:', seasonViewMode); + return ( + + onSeasonChange(season)} + > + + + {selectedSeason === season && ( + + )} + {/* Show episode count badge, including when there are no episodes */} + + + {seasonEpisodes.length} ep{seasonEpisodes.length !== 1 ? 's' : ''} + + + + + Season {season} + + + ); }} keyExtractor={season => season.toString()} @@ -709,10 +941,10 @@ export const SeriesContent: React.FC = ({ {/* Only render episode list if there are episodes */} {currentSeasonEpisodes.length > 0 && ( (settings?.episodeLayoutStyle === 'horizontal') ? ( - // Horizontal Layout (Netflix-style) - ( = ({ horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={isTablet ? styles.episodeListContentHorizontalTablet : styles.episodeListContentHorizontal} - estimatedItemSize={(isTablet ? width * 0.4 : width * 0.75) + (isTablet ? 20 : 16)} - estimatedListSize={{ width: Dimensions.get('window').width, height: isTablet ? 260 + 24 : 200 + 20 }} - overrideItemLayout={(layout, _item, _index) => { + removeClippedSubviews + initialNumToRender={3} + maxToRenderPerBatch={5} + windowSize={5} + getItemLayout={(data, index) => { const cardWidth = isTablet ? width * 0.4 : width * 0.75; const margin = isTablet ? 20 : 16; - layout.size = cardWidth + margin; - layout.span = 1; + return { + length: cardWidth + margin, + offset: (cardWidth + margin) * index, + index, + }; }} - removeClippedSubviews /> ) : ( - // Vertical Layout (Traditional) + // Vertical Layout (Traditional) - Using FlashList = ({ )} keyExtractor={episode => episode.id.toString()} contentContainerStyle={isTablet ? styles.episodeListContentVerticalTablet : styles.episodeListContentVertical} - estimatedItemSize={isTablet ? 160 + 16 : 120 + 16} - estimatedListSize={{ width: Dimensions.get('window').width, height: isTablet ? 160 + 16 : 120 + 16 }} - overrideItemLayout={(layout, _item, _index) => { - // height along main axis for vertical list - const itemHeight = (isTablet ? 160 : 120) + 16; // card height + marginBottom - layout.size = itemHeight; - layout.span = 1; - }} removeClippedSubviews /> ) @@ -1148,15 +1376,21 @@ const styles = StyleSheet.create({ marginBottom: 24, paddingHorizontal: 24, }, + seasonSelectorHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, seasonSelectorTitle: { fontSize: 18, fontWeight: '600', - marginBottom: 12, + marginBottom: 0, // Removed margin bottom here }, seasonSelectorTitleTablet: { fontSize: 22, fontWeight: '700', - marginBottom: 16, + marginBottom: 0, // Removed margin bottom here }, seasonSelectorContainer: { flexGrow: 0, @@ -1228,6 +1462,62 @@ const styles = StyleSheet.create({ selectedSeasonButtonTextTablet: { fontWeight: '800', }, + seasonViewToggle: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 6, + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.2)', + }, + seasonViewToggleText: { + fontSize: 12, + fontWeight: '500', + marginRight: 4, + }, + seasonTextButton: { + alignItems: 'center', + marginRight: 16, + width: 110, + justifyContent: 'center', + paddingVertical: 12, + paddingHorizontal: 16, + borderRadius: 12, + backgroundColor: 'transparent', + }, + seasonTextButtonTablet: { + alignItems: 'center', + marginRight: 20, + width: 130, + justifyContent: 'center', + paddingVertical: 14, + paddingHorizontal: 18, + borderRadius: 14, + backgroundColor: 'transparent', + }, + selectedSeasonTextButton: { + backgroundColor: 'rgba(255,255,255,0.08)', + }, + seasonTextButtonText: { + fontSize: 15, + fontWeight: '600', + letterSpacing: 0.3, + textAlign: 'center', + }, + seasonTextButtonTextTablet: { + fontSize: 17, + fontWeight: '600', + letterSpacing: 0.4, + textAlign: 'center', + }, + selectedSeasonTextButtonText: { + fontWeight: '700', + }, + selectedSeasonTextButtonTextTablet: { + fontWeight: '800', + }, episodeCountBadge: { position: 'absolute', top: 8, diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index a84d942..9e02744 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -675,6 +675,26 @@ const SettingsScreen: React.FC = () => { )} + + {/* Discord Join Button - Show on all categories for tablet */} + + Linking.openURL('https://discord.gg/6w8dr3TSDN')} + activeOpacity={0.7} + > + + + + Join Discord + + + + @@ -716,6 +736,26 @@ const SettingsScreen: React.FC = () => { Made with ❤️ by the Nuvio team + + {/* Discord Join Button */} + + Linking.openURL('https://discord.gg/6w8dr3TSDN')} + activeOpacity={0.7} + > + + + + Join Discord + + + + @@ -959,6 +999,34 @@ const styles = StyleSheet.create({ fontSize: 14, opacity: 0.5, }, + // New styles for Discord button + discordContainer: { + marginTop: 20, + marginBottom: 20, + alignItems: 'center', + }, + discordButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 8, + maxWidth: 200, + }, + discordButtonContent: { + flexDirection: 'row', + alignItems: 'center', + }, + discordLogo: { + width: 16, + height: 16, + marginRight: 8, + }, + discordButtonText: { + fontSize: 14, + fontWeight: '500', + }, }); export default SettingsScreen; \ No newline at end of file