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