diff --git a/src/components/metadata/.HeroSection.tsx.swp b/src/components/metadata/.HeroSection.tsx.swp new file mode 100644 index 00000000..6a8c4627 Binary files /dev/null and b/src/components/metadata/.HeroSection.tsx.swp differ diff --git a/src/screens/OnboardingScreen.tsx b/src/screens/OnboardingScreen.tsx index c99125fc..fc682024 100644 --- a/src/screens/OnboardingScreen.tsx +++ b/src/screens/OnboardingScreen.tsx @@ -5,28 +5,21 @@ import { StyleSheet, Dimensions, TouchableOpacity, - FlatList, - Image, StatusBar, Platform, } from 'react-native'; -import { MaterialIcons } from '@expo/vector-icons'; -import { LinearGradient } from 'expo-linear-gradient'; import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, - withRepeat, - withSequence, - FadeInDown, + FadeIn, FadeInUp, useAnimatedScrollHandler, - runOnJS, - interpolateColor, interpolate, Extrapolation, - useAnimatedReaction, + runOnJS, + SharedValue, } from 'react-native-reanimated'; import { useTheme } from '../contexts/ThemeContext'; import { NavigationProp, useNavigation } from '@react-navigation/native'; @@ -35,204 +28,10 @@ import { mmkvStorage } from '../services/mmkvStorage'; const { width, height } = Dimensions.get('window'); -// Animation configuration const SPRING_CONFIG = { - damping: 15, - stiffness: 150, - mass: 1, -}; - -const SLIDE_TIMING = { - duration: 400, -}; - -// Animated Button Component -const AnimatedButton = ({ - onPress, - backgroundColor, - text, - icon, -}: { - onPress: () => void; - backgroundColor: string; - text: string; - icon: string; -}) => { - const scale = useSharedValue(1); - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ scale: scale.value }], - })); - - const handlePressIn = () => { - scale.value = withSpring(0.95, SPRING_CONFIG); - }; - - const handlePressOut = () => { - scale.value = withSpring(1, SPRING_CONFIG); - }; - - return ( - - - {text} - - - - ); -}; - -// Slide Content Component with animations -const SlideContent = ({ item, isActive }: { item: OnboardingSlide; isActive: boolean }) => { - // Premium icon animations: scale, floating, rotation, and glow - const iconScale = useSharedValue(isActive ? 1 : 0.8); - const iconOpacity = useSharedValue(isActive ? 1 : 0); - const iconTranslateY = useSharedValue(isActive ? 0 : 20); - const iconRotation = useSharedValue(0); - const glowIntensity = useSharedValue(isActive ? 1 : 0); - - React.useEffect(() => { - if (isActive) { - iconScale.value = withSpring(1.1, SPRING_CONFIG); - iconOpacity.value = withTiming(1, SLIDE_TIMING); - iconTranslateY.value = withSpring(0, SPRING_CONFIG); - iconRotation.value = withSpring(0, SPRING_CONFIG); - glowIntensity.value = withSpring(1, SPRING_CONFIG); - } else { - iconScale.value = 0.8; - iconOpacity.value = 0; - iconTranslateY.value = 20; - iconRotation.value = -15; - glowIntensity.value = 0; - } - }, [isActive]); - - const animatedIconStyle = useAnimatedStyle(() => { - return { - transform: [ - { scale: iconScale.value }, - { translateY: iconTranslateY.value }, - { rotate: `${iconRotation.value}deg` }, - ], - opacity: iconOpacity.value, - }; - }); - - // Premium floating animation for active icon - const floatAnim = useSharedValue(0); - React.useEffect(() => { - if (isActive) { - floatAnim.value = withRepeat( - withSequence( - withTiming(10, { duration: 2500 }), - withTiming(-10, { duration: 2500 }) - ), - -1, - true - ); - } else { - floatAnim.value = 0; - } - }, [isActive]); - - const floatingIconStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: floatAnim.value }], - })); - - // Glow animation with pulse effect - const pulseAnim = useSharedValue(1); - React.useEffect(() => { - if (isActive) { - pulseAnim.value = withRepeat( - withSequence( - withTiming(1.3, { duration: 2000 }), - withTiming(1.1, { duration: 2000 }) - ), - -1, - true - ); - } - }, [isActive]); - - const animatedGlowStyle = useAnimatedStyle(() => ({ - opacity: glowIntensity.value * 0.5, - transform: [{ scale: pulseAnim.value * 1.2 + iconScale.value * 0.3 }], - })); - - return ( - - {/* Premium glow effect */} - - - - - - - - - - - - - {item.title} - - - {item.subtitle} - - - {item.description} - - - - ); + damping: 20, + stiffness: 90, + mass: 0.8, }; interface OnboardingSlide { @@ -240,90 +39,180 @@ interface OnboardingSlide { title: string; subtitle: string; description: string; - icon: keyof typeof MaterialIcons.glyphMap; - gradient: [string, string]; } const onboardingData: OnboardingSlide[] = [ { id: '1', - title: 'Welcome to Nuvio', + title: 'Welcome to\nNuvio', subtitle: 'Your Ultimate Content Hub', description: 'Discover, organize, and manage your favorite movies and TV shows from multiple sources in one beautiful app.', - icon: 'play-circle-filled', - gradient: ['#667eea', '#764ba2'], }, { id: '2', - title: 'Powerful Addons', + title: 'Powerful\nAddons', subtitle: 'Extend Your Experience', description: 'Install addons to access content from various platforms and services. Choose what works best for you.', - icon: 'extension', - gradient: ['#f093fb', '#f5576c'], }, { id: '3', - title: 'Smart Discovery', + title: 'Smart\nDiscovery', subtitle: 'Find What You Love', description: 'Browse trending content, search across all your sources, and get personalized recommendations.', - icon: 'explore', - gradient: ['#4facfe', '#00f2fe'], }, { id: '4', - title: 'Your Library', + title: 'Your\nLibrary', subtitle: 'Track & Organize', description: 'Save favorites, track your progress, and sync with Trakt to keep everything organized across devices.', - icon: 'library-books', - gradient: ['#43e97b', '#38f9d7'], }, ]; +// Animated Slide Component with parallax +const AnimatedSlide = ({ + item, + index, + scrollX +}: { + item: OnboardingSlide; + index: number; + scrollX: SharedValue; +}) => { + const inputRange = [(index - 1) * width, index * width, (index + 1) * width]; + + const titleStyle = useAnimatedStyle(() => { + const translateX = interpolate( + scrollX.value, + inputRange, + [width * 0.3, 0, -width * 0.3], + Extrapolation.CLAMP + ); + const opacity = interpolate( + scrollX.value, + inputRange, + [0, 1, 0], + Extrapolation.CLAMP + ); + const scale = interpolate( + scrollX.value, + inputRange, + [0.8, 1, 0.8], + Extrapolation.CLAMP + ); + return { + transform: [{ translateX }, { scale }], + opacity, + }; + }); + + const subtitleStyle = useAnimatedStyle(() => { + const translateX = interpolate( + scrollX.value, + inputRange, + [width * 0.5, 0, -width * 0.5], + Extrapolation.CLAMP + ); + const opacity = interpolate( + scrollX.value, + inputRange, + [0, 1, 0], + Extrapolation.CLAMP + ); + return { + transform: [{ translateX }], + opacity, + }; + }); + + const descriptionStyle = useAnimatedStyle(() => { + const translateX = interpolate( + scrollX.value, + inputRange, + [width * 0.7, 0, -width * 0.7], + Extrapolation.CLAMP + ); + const opacity = interpolate( + scrollX.value, + inputRange, + [0, 1, 0], + Extrapolation.CLAMP + ); + return { + transform: [{ translateX }], + opacity, + }; + }); + + return ( + + + + {item.title} + + + + {item.subtitle} + + + + {item.description} + + + + ); +}; + const OnboardingScreen = () => { const { currentTheme } = useTheme(); const navigation = useNavigation>(); const [currentIndex, setCurrentIndex] = useState(0); - const flatListRef = useRef(null); - const progressValue = useSharedValue(0); + const flatListRef = useRef>(null); const scrollX = useSharedValue(0); - const currentSlide = onboardingData[currentIndex]; - // Update progress when index changes - React.useEffect(() => { - progressValue.value = withSpring( - (currentIndex + 1) / onboardingData.length, - SPRING_CONFIG - ); - }, [currentIndex]); + const updateIndex = (index: number) => { + setCurrentIndex(index); + }; const onScroll = useAnimatedScrollHandler({ onScroll: (event) => { scrollX.value = event.contentOffset.x; }, + onMomentumEnd: (event) => { + const slideIndex = Math.round(event.contentOffset.x / width); + runOnJS(updateIndex)(slideIndex); + }, }); - const animatedProgressStyle = useAnimatedStyle(() => ({ - width: `${progressValue.value * 100}%`, - })); + const progressStyle = useAnimatedStyle(() => { + const progress = interpolate( + scrollX.value, + [0, (onboardingData.length - 1) * width], + [0, 100], + Extrapolation.CLAMP + ); + return { + width: `${progress}%`, + }; + }); const handleNext = () => { if (currentIndex < onboardingData.length - 1) { const nextIndex = currentIndex + 1; - setCurrentIndex(nextIndex); - flatListRef.current?.scrollToIndex({ index: nextIndex, animated: true }); - progressValue.value = (nextIndex + 1) / onboardingData.length; + flatListRef.current?.scrollToOffset({ + offset: nextIndex * width, + animated: true + }); } else { handleGetStarted(); } }; const handleSkip = () => { - // Skip login: proceed to app and show a one-time hint toast (async () => { try { await mmkvStorage.setItem('hasCompletedOnboarding', 'true'); await mmkvStorage.setItem('showLoginHintToastOnce', 'true'); - } catch {} + } catch { } navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] }); })(); }; @@ -331,7 +220,6 @@ const OnboardingScreen = () => { const handleGetStarted = async () => { try { await mmkvStorage.setItem('hasCompletedOnboarding', 'true'); - // After onboarding, go directly to main app navigation.reset({ index: 0, routes: [{ name: 'MainTabs' }] }); } catch (error) { if (__DEV__) console.error('Error saving onboarding status:', error); @@ -339,120 +227,71 @@ const OnboardingScreen = () => { } }; - const renderSlide = ({ item, index }: { item: OnboardingSlide; index: number }) => { - const isActive = index === currentIndex; - - return ( - - ); - }; - - const renderPaginationDot = (index: number) => { - const scale = useSharedValue(index === currentIndex ? 1 : 0.8); - const opacity = useSharedValue(index === currentIndex ? 1 : 0.4); - - React.useEffect(() => { - scale.value = withSpring( - index === currentIndex ? 1.3 : 0.8, - SPRING_CONFIG - ); - opacity.value = withTiming( - index === currentIndex ? 1 : 0.4, - SLIDE_TIMING - ); - }, [currentIndex, index]); - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ scale: scale.value }], - opacity: opacity.value, - })); - - return ( - - ); - }; - - const renderPagination = () => ( - - {onboardingData.map((_, index) => renderPaginationDot(index))} - + const renderSlide = ({ item, index }: { item: OnboardingSlide; index: number }) => ( + ); - // Background slide styles - const getBackgroundSlideStyle = (index: number) => { - 'worklet'; - return useAnimatedStyle(() => { + // Animated pagination dots + const PaginationDot = ({ index }: { index: number }) => { + const dotStyle = useAnimatedStyle(() => { const inputRange = [(index - 1) * width, index * width, (index + 1) * width]; - const slideOpacity = interpolate( + const dotWidth = interpolate( scrollX.value, inputRange, - [0, 1, 0], + [8, 32, 8], Extrapolation.CLAMP ); - - return { opacity: slideOpacity }; + const opacity = interpolate( + scrollX.value, + inputRange, + [0.3, 1, 0.3], + Extrapolation.CLAMP + ); + return { + width: dotWidth, + opacity, + }; }); + + return ; + }; + + // Animated button + const buttonScale = useSharedValue(1); + + const buttonStyle = useAnimatedStyle(() => ({ + transform: [{ scale: buttonScale.value }], + })); + + const handlePressIn = () => { + buttonScale.value = withSpring(0.95, { damping: 15, stiffness: 400 }); + }; + + const handlePressOut = () => { + buttonScale.value = withSpring(1, { damping: 15, stiffness: 400 }); }; return ( - {/* Animated gradient background that transitions between slides */} - - {onboardingData.map((slide, index) => ( - - - - - ))} - - - + - {/* Content container with status bar padding */} {/* Header */} - + - - Skip - + Skip - {/* Progress Bar */} - - + {/* Smooth Progress Bar */} + + - + - {/* Content */} + {/* Slides */} { keyExtractor={(item) => item.id} onScroll={onScroll} scrollEventThrottle={16} - onMomentumScrollEnd={(event) => { - const slideIndex = Math.round(event.nativeEvent.contentOffset.x / width); - setCurrentIndex(slideIndex); - }} + decelerationRate="fast" + snapToInterval={width} + snapToAlignment="start" + bounces={false} style={{ flex: 1 }} /> {/* Footer */} - - {renderPagination()} - - - + + {/* Smooth Pagination */} + + {onboardingData.map((_, index) => ( + + ))} - + + {/* Animated Button */} + + + + {currentIndex === onboardingData.length - 1 ? 'Get Started' : 'Continue'} + + + + ); @@ -491,146 +343,100 @@ const OnboardingScreen = () => { const styles = StyleSheet.create({ container: { flex: 1, + backgroundColor: '#0A0A0A', + }, + fullScreenContainer: { + flex: 1, + paddingTop: Platform.OS === 'ios' ? 60 : (StatusBar.currentHeight || 24) + 16, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingHorizontal: 20, - paddingTop: 10, + paddingHorizontal: 24, paddingBottom: 20, }, skipButton: { - padding: 10, + paddingVertical: 8, + paddingHorizontal: 4, }, skipText: { - fontSize: 16, + fontSize: 15, fontWeight: '500', + color: 'rgba(255, 255, 255, 0.4)', }, progressContainer: { flex: 1, - height: 4, - borderRadius: 2, - marginHorizontal: 20, + height: 3, + borderRadius: 1.5, + marginLeft: 24, + backgroundColor: 'rgba(255, 255, 255, 0.08)', overflow: 'hidden', }, progressBar: { height: '100%', - borderRadius: 2, + borderRadius: 1.5, + backgroundColor: '#FFFFFF', }, slide: { width, - height: '100%', - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 40, - }, - fullScreenContainer: { flex: 1, - paddingTop: Platform.OS === 'ios' ? 44 : StatusBar.currentHeight || 24, - }, - glowContainer: { - position: 'absolute', - width: 200, - height: 200, - borderRadius: 100, - alignItems: 'center', justifyContent: 'center', - top: '35%', - }, - glowCircle: { - width: '100%', - height: '100%', - borderRadius: 100, - opacity: 0.4, - }, - iconContainer: { - width: 180, - height: 180, - borderRadius: 90, - alignItems: 'center', - justifyContent: 'center', - marginBottom: 60, - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 15, - }, - shadowOpacity: 0.5, - shadowRadius: 25, - elevation: 20, - }, - iconWrapper: { - alignItems: 'center', - justifyContent: 'center', - zIndex: 10, + paddingHorizontal: 32, }, textContainer: { - alignItems: 'center', - paddingHorizontal: 20, + alignItems: 'flex-start', }, title: { - fontSize: 32, - fontWeight: 'bold', - textAlign: 'center', - marginBottom: 12, + fontSize: 52, + fontWeight: '800', + letterSpacing: -2, + lineHeight: 56, + marginBottom: 16, + color: '#FFFFFF', }, subtitle: { - fontSize: 20, + fontSize: 17, fontWeight: '600', - textAlign: 'center', + color: 'rgba(255, 255, 255, 0.6)', marginBottom: 20, + letterSpacing: 0.3, }, description: { - fontSize: 16, - textAlign: 'center', + fontSize: 15, lineHeight: 24, + color: 'rgba(255, 255, 255, 0.4)', maxWidth: 300, }, footer: { - paddingHorizontal: 20, - paddingBottom: Platform.OS === 'ios' ? 40 : 20, + paddingHorizontal: 24, + paddingBottom: Platform.OS === 'ios' ? 50 : 32, }, pagination: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', - marginBottom: 40, + marginBottom: 32, + gap: 6, }, paginationDot: { - width: 10, - height: 10, - borderRadius: 5, - marginHorizontal: 6, - }, - buttonContainer: { - alignItems: 'center', + height: 8, + borderRadius: 4, + backgroundColor: '#FFFFFF', }, button: { - borderRadius: 30, - paddingVertical: 16, - paddingHorizontal: 32, - minWidth: 160, + backgroundColor: '#FFFFFF', + borderRadius: 16, + paddingVertical: 18, alignItems: 'center', justifyContent: 'center', - flexDirection: 'row', - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 4, - }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 8, }, - nextButton: {}, buttonText: { fontSize: 16, - fontWeight: '600', - }, - buttonIcon: { - marginLeft: 8, + fontWeight: '700', + color: '#0A0A0A', + letterSpacing: 0.3, }, }); -export default OnboardingScreen; \ No newline at end of file +export default OnboardingScreen; \ No newline at end of file