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) => {