diff --git a/android/app/build.gradle b/android/app/build.gradle index 39240f0..0a09bd6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -95,9 +95,6 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0.0" - ndk { - abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64" - } } signingConfigs { debug { @@ -129,16 +126,6 @@ android { androidResources { ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~' } - - // --- ABI SPLITS: generate separate APKs/AABs for each CPU architecture (plus a universal build) --- - splits { - abi { - enable true // turn on ABI splits - reset() // start from a clean slate - include "armeabi-v7a", "arm64-v8a", "x86", "x86_64" - universalApk true // also generate a universal APK that contains all ABIs - } - } } // Apply static values from `gradle.properties` to the `android.packagingOptions` diff --git a/ios/Nuvio.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Nuvio.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Nuvio.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Nuvio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Nuvio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Nuvio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 8e69eff..a2c925a 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; -import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text } from 'react-native'; +import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet } from 'react-native'; import Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; @@ -10,8 +10,11 @@ import { storageService } from '../../services/storageService'; import { logger } from '../../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; import { useTraktAutosync } from '../../hooks/useTraktAutosync'; import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; +import { useMetadata } from '../../hooks/useMetadata'; +import { useSettings } from '../../hooks/useSettings'; import { DEFAULT_SUBTITLE_SIZE, @@ -62,7 +65,8 @@ const AndroidVideoPlayer: React.FC = () => { type, episodeId, imdbId, - availableStreams: passedAvailableStreams + availableStreams: passedAvailableStreams, + backdrop } = route.params; // Initialize Trakt autosync @@ -157,7 +161,25 @@ const AndroidVideoPlayer: React.FC = () => { const isMounted = useRef(true); const controlsTimeout = useRef(null); const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false); - // Offset in seconds to avoid seeking to the exact end, which fires onEnd and resets. + // Get metadata to access logo (only if we have a valid id) + const shouldLoadMetadata = Boolean(id && type); + const metadataResult = useMetadata({ + id: id || 'placeholder', + type: type || 'movie' + }); + const { metadata, loading: metadataLoading } = shouldLoadMetadata ? metadataResult : { metadata: null, loading: false }; + const { settings } = useSettings(); + + // Logo animation values + const logoScaleAnim = useRef(new Animated.Value(0.8)).current; + const logoOpacityAnim = useRef(new Animated.Value(0)).current; + const pulseAnim = useRef(new Animated.Value(1)).current; + + // Check if we have a logo to show + const hasLogo = metadata && metadata.logo && !metadataLoading; + + // Small offset (in seconds) used to avoid seeking to the *exact* end of the + // file which triggers the `onEnd` callback and causes playback to restart. const END_EPSILON = 0.3; const hideControls = () => { @@ -241,7 +263,51 @@ const AndroidVideoPlayer: React.FC = () => { }, []); const startOpeningAnimation = () => { - // Animation logic here + // Logo entrance animation + Animated.parallel([ + Animated.timing(logoOpacityAnim, { + toValue: 1, + duration: 600, + useNativeDriver: true, + }), + Animated.spring(logoScaleAnim, { + toValue: 1, + tension: 50, + friction: 8, + useNativeDriver: true, + }), + ]).start(); + + // Continuous pulse animation for the logo + const createPulseAnimation = () => { + return Animated.sequence([ + Animated.timing(pulseAnim, { + toValue: 1.05, + duration: 1000, + useNativeDriver: true, + }), + Animated.timing(pulseAnim, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }), + ]); + }; + + const loopPulse = () => { + createPulseAnimation().start(() => { + if (!isOpeningAnimationComplete) { + loopPulse(); + } + }); + }; + + // Start pulsing after a short delay + setTimeout(() => { + if (!isOpeningAnimationComplete) { + loopPulse(); + } + }, 800); }; const completeOpeningAnimation = () => { @@ -951,6 +1017,25 @@ const AndroidVideoPlayer: React.FC = () => { ]} pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'} > + {backdrop && ( + + )} + + { + {hasLogo ? ( + + + + ) : ( + <> - Loading video... + + )} diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 906fd90..9e8a191 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect } from 'react'; -import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text } from 'react-native'; +import { View, TouchableOpacity, Dimensions, Animated, ActivityIndicator, Platform, NativeModules, StatusBar, Text, Image, StyleSheet } from 'react-native'; import { VLCPlayer } from 'react-native-vlc-media-player'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { RootStackParamList } from '../../navigation/AppNavigator'; @@ -10,16 +10,19 @@ import { storageService } from '../../services/storageService'; import { logger } from '../../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { MaterialIcons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; import AndroidVideoPlayer from './AndroidVideoPlayer'; import { useTraktAutosync } from '../../hooks/useTraktAutosync'; import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; +import { useMetadata } from '../../hooks/useMetadata'; +import { useSettings } from '../../hooks/useSettings'; import { DEFAULT_SUBTITLE_SIZE, AudioTrack, TextTrack, ResizeModeType, - WyzieSubtitle, + WyzieSubtitle, SubtitleCue, RESUME_PREF_KEY, RESUME_PREF, @@ -57,7 +60,8 @@ const VideoPlayer: React.FC = () => { type, episodeId, imdbId, - availableStreams: passedAvailableStreams + availableStreams: passedAvailableStreams, + backdrop } = route.params; // Initialize Trakt autosync @@ -152,6 +156,24 @@ const VideoPlayer: React.FC = () => { const isMounted = useRef(true); const controlsTimeout = useRef(null); const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false); + + // Get metadata to access logo (only if we have a valid id) + const shouldLoadMetadata = Boolean(id && type); + const metadataResult = useMetadata({ + id: id || 'placeholder', + type: type || 'movie' + }); + const { metadata, loading: metadataLoading } = shouldLoadMetadata ? metadataResult : { metadata: null, loading: false }; + const { settings } = useSettings(); + + // Logo animation values + const logoScaleAnim = useRef(new Animated.Value(0.8)).current; + const logoOpacityAnim = useRef(new Animated.Value(0)).current; + const pulseAnim = useRef(new Animated.Value(1)).current; + + // Check if we have a logo to show + const hasLogo = metadata && metadata.logo && !metadataLoading; + // Small offset (in seconds) used to avoid seeking to the *exact* end of the // file which triggers the `onEnd` callback and causes playback to restart. const END_EPSILON = 0.3; @@ -237,7 +259,51 @@ const VideoPlayer: React.FC = () => { }, []); const startOpeningAnimation = () => { - // Animation logic here + // Logo entrance animation + Animated.parallel([ + Animated.timing(logoOpacityAnim, { + toValue: 1, + duration: 600, + useNativeDriver: true, + }), + Animated.spring(logoScaleAnim, { + toValue: 1, + tension: 50, + friction: 8, + useNativeDriver: true, + }), + ]).start(); + + // Continuous pulse animation for the logo + const createPulseAnimation = () => { + return Animated.sequence([ + Animated.timing(pulseAnim, { + toValue: 1.05, + duration: 1000, + useNativeDriver: true, + }), + Animated.timing(pulseAnim, { + toValue: 1, + duration: 1000, + useNativeDriver: true, + }), + ]); + }; + + const loopPulse = () => { + createPulseAnimation().start(() => { + if (!isOpeningAnimationComplete) { + loopPulse(); + } + }); + }; + + // Start pulsing after a short delay + setTimeout(() => { + if (!isOpeningAnimationComplete) { + loopPulse(); + } + }, 800); }; const completeOpeningAnimation = () => { @@ -389,7 +455,6 @@ const VideoPlayer: React.FC = () => { // Calculate position as fraction const position = timeInSeconds / duration; vlcRef.current.seek(position); - // Clear seek state after Android seek setTimeout(() => { if (isMounted.current) { @@ -400,12 +465,18 @@ const VideoPlayer: React.FC = () => { } }, 500); } else { - // iOS fallback - use seek prop - const position = timeInSeconds / duration; - setSeekPosition(position); - + // iOS (and other platforms) – prefer direct seek on the ref to avoid re-mounts caused by the `seek` prop + const position = timeInSeconds / duration; // VLC expects a 0-1 fraction + if (vlcRef.current && typeof vlcRef.current.seek === 'function') { + vlcRef.current.seek(position); + } else { + // Fallback to legacy behaviour only if direct seek is unavailable + setSeekPosition(position); + } + setTimeout(() => { if (isMounted.current) { + // Reset temporary seek state setSeekPosition(null); isSeeking.current = false; if (DEBUG_MODE) { @@ -967,6 +1038,25 @@ const VideoPlayer: React.FC = () => { ]} pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'} > + {backdrop && ( + + )} + + { - - Loading video... + {hasLogo ? ( + + + + ) : ( + <> + + + )} @@ -1055,7 +1165,6 @@ const VideoPlayer: React.FC = () => { resizeMode={resizeMode as any} audioTrack={selectedAudioTrack ?? undefined} textTrack={useCustomSubtitles ? -1 : (selectedTextTrack ?? undefined)} - seek={Platform.OS === 'ios' ? (seekPosition ?? undefined) : undefined} autoAspectRatio /> diff --git a/src/components/player/utils/playerStyles.ts b/src/components/player/utils/playerStyles.ts index 84cea74..746f27f 100644 --- a/src/components/player/utils/playerStyles.ts +++ b/src/components/player/utils/playerStyles.ts @@ -474,7 +474,6 @@ export const styles = StyleSheet.create({ }, openingContent: { padding: 20, - backgroundColor: 'rgba(0,0,0,0.85)', borderRadius: 10, justifyContent: 'center', alignItems: 'center', diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 3f1bd52..ba6944f 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -66,6 +66,7 @@ export type RootStackParamList = { type: string; stream: Stream; episodeId?: string; + backdrop?: string; }; Player: { uri: string; @@ -82,6 +83,7 @@ export type RootStackParamList = { episodeId?: string; imdbId?: string; availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; + backdrop?: string; }; Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string }; Credits: { mediaId: string; mediaType: string }; diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 8eafdb1..4b05088 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -24,6 +24,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import { Image } from 'expo-image'; import { RootStackParamList, RootStackNavigationProp } from '../navigation/AppNavigator'; import { useMetadata } from '../hooks/useMetadata'; +import { useMetadataAssets } from '../hooks/useMetadataAssets'; import { useTheme } from '../contexts/ThemeContext'; import { Stream } from '../types/metadata'; import { tmdbService } from '../services/tmdbService'; @@ -274,6 +275,11 @@ export const StreamsScreen = () => { imdbId, } = useMetadata({ id, type }); + // Get backdrop from metadata assets + const setMetadataStub = useCallback(() => {}, []); + const memoizedSettings = useMemo(() => settings, [settings.logoSourcePreference, settings.tmdbLanguagePreference]); + const { bannerImage } = useMetadataAssets(metadata, id, type, imdbId, memoizedSettings, setMetadataStub); + // Create styles using current theme colors const styles = React.useMemo(() => createStyles(colors), [colors]); @@ -578,6 +584,7 @@ export const StreamsScreen = () => { episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, imdbId: imdbId || undefined, availableStreams: streamsToPass, + backdrop: bannerImage || undefined, }); } catch (error) { logger.error('[StreamsScreen] Error locking orientation before navigation:', error); @@ -598,9 +605,10 @@ export const StreamsScreen = () => { episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, imdbId: imdbId || undefined, availableStreams: streamsToPass, + backdrop: bannerImage || undefined, }); } - }, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams]); + }, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams, bannerImage]); // Update handleStreamPress const handleStreamPress = useCallback(async (stream: Stream) => {