mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
imporved behaviour of videoplayer and added a nice loading screen
This commit is contained in:
parent
ab7134bd3a
commit
379bcc7507
8 changed files with 257 additions and 32 deletions
|
|
@ -95,9 +95,6 @@ android {
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0.0"
|
versionName "1.0.0"
|
||||||
ndk {
|
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
|
|
@ -129,16 +126,6 @@ android {
|
||||||
androidResources {
|
androidResources {
|
||||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~'
|
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`
|
// Apply static values from `gradle.properties` to the `android.packagingOptions`
|
||||||
|
|
|
||||||
7
ios/Nuvio.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
7
ios/Nuvio.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
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 Video, { VideoRef, SelectedTrack, SelectedTrackType, BufferingStrategyType } from 'react-native-video';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
|
|
@ -10,8 +10,11 @@ import { storageService } from '../../services/storageService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
||||||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||||
|
import { useMetadata } from '../../hooks/useMetadata';
|
||||||
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SUBTITLE_SIZE,
|
DEFAULT_SUBTITLE_SIZE,
|
||||||
|
|
@ -62,7 +65,8 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
type,
|
type,
|
||||||
episodeId,
|
episodeId,
|
||||||
imdbId,
|
imdbId,
|
||||||
availableStreams: passedAvailableStreams
|
availableStreams: passedAvailableStreams,
|
||||||
|
backdrop
|
||||||
} = route.params;
|
} = route.params;
|
||||||
|
|
||||||
// Initialize Trakt autosync
|
// Initialize Trakt autosync
|
||||||
|
|
@ -157,7 +161,25 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
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 END_EPSILON = 0.3;
|
||||||
|
|
||||||
const hideControls = () => {
|
const hideControls = () => {
|
||||||
|
|
@ -241,7 +263,51 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startOpeningAnimation = () => {
|
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 = () => {
|
const completeOpeningAnimation = () => {
|
||||||
|
|
@ -951,6 +1017,25 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
|
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
|
||||||
>
|
>
|
||||||
|
{backdrop && (
|
||||||
|
<Image
|
||||||
|
source={{ uri: backdrop }}
|
||||||
|
style={[StyleSheet.absoluteFill, { width: screenDimensions.width, height: screenDimensions.height }]}
|
||||||
|
resizeMode="cover"
|
||||||
|
blurRadius={0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<LinearGradient
|
||||||
|
colors={[
|
||||||
|
'rgba(0,0,0,0.3)',
|
||||||
|
'rgba(0,0,0,0.6)',
|
||||||
|
'rgba(0,0,0,0.8)',
|
||||||
|
'rgba(0,0,0,0.9)'
|
||||||
|
]}
|
||||||
|
locations={[0, 0.3, 0.7, 1]}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.loadingCloseButton}
|
style={styles.loadingCloseButton}
|
||||||
onPress={handleClose}
|
onPress={handleClose}
|
||||||
|
|
@ -960,8 +1045,28 @@ const AndroidVideoPlayer: React.FC = () => {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.openingContent}>
|
<View style={styles.openingContent}>
|
||||||
|
{hasLogo ? (
|
||||||
|
<Animated.View style={{
|
||||||
|
transform: [
|
||||||
|
{ scale: Animated.multiply(logoScaleAnim, pulseAnim) }
|
||||||
|
],
|
||||||
|
opacity: logoOpacityAnim,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: metadata.logo }}
|
||||||
|
style={{
|
||||||
|
width: 300,
|
||||||
|
height: 180,
|
||||||
|
resizeMode: 'contain',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<ActivityIndicator size="large" color="#E50914" />
|
<ActivityIndicator size="large" color="#E50914" />
|
||||||
<Text style={styles.openingText}>Loading video...</Text>
|
</>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
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 { VLCPlayer } from 'react-native-vlc-media-player';
|
||||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
|
|
@ -10,16 +10,19 @@ import { storageService } from '../../services/storageService';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import AndroidVideoPlayer from './AndroidVideoPlayer';
|
import AndroidVideoPlayer from './AndroidVideoPlayer';
|
||||||
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
import { useTraktAutosync } from '../../hooks/useTraktAutosync';
|
||||||
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings';
|
||||||
|
import { useMetadata } from '../../hooks/useMetadata';
|
||||||
|
import { useSettings } from '../../hooks/useSettings';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_SUBTITLE_SIZE,
|
DEFAULT_SUBTITLE_SIZE,
|
||||||
AudioTrack,
|
AudioTrack,
|
||||||
TextTrack,
|
TextTrack,
|
||||||
ResizeModeType,
|
ResizeModeType,
|
||||||
WyzieSubtitle,
|
WyzieSubtitle,
|
||||||
SubtitleCue,
|
SubtitleCue,
|
||||||
RESUME_PREF_KEY,
|
RESUME_PREF_KEY,
|
||||||
RESUME_PREF,
|
RESUME_PREF,
|
||||||
|
|
@ -57,7 +60,8 @@ const VideoPlayer: React.FC = () => {
|
||||||
type,
|
type,
|
||||||
episodeId,
|
episodeId,
|
||||||
imdbId,
|
imdbId,
|
||||||
availableStreams: passedAvailableStreams
|
availableStreams: passedAvailableStreams,
|
||||||
|
backdrop
|
||||||
} = route.params;
|
} = route.params;
|
||||||
|
|
||||||
// Initialize Trakt autosync
|
// Initialize Trakt autosync
|
||||||
|
|
@ -152,6 +156,24 @@ const VideoPlayer: React.FC = () => {
|
||||||
const isMounted = useRef(true);
|
const isMounted = useRef(true);
|
||||||
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
const controlsTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [isSyncingBeforeClose, setIsSyncingBeforeClose] = useState(false);
|
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
|
// 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.
|
// file which triggers the `onEnd` callback and causes playback to restart.
|
||||||
const END_EPSILON = 0.3;
|
const END_EPSILON = 0.3;
|
||||||
|
|
@ -237,7 +259,51 @@ const VideoPlayer: React.FC = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const startOpeningAnimation = () => {
|
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 = () => {
|
const completeOpeningAnimation = () => {
|
||||||
|
|
@ -389,7 +455,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
// Calculate position as fraction
|
// Calculate position as fraction
|
||||||
const position = timeInSeconds / duration;
|
const position = timeInSeconds / duration;
|
||||||
vlcRef.current.seek(position);
|
vlcRef.current.seek(position);
|
||||||
|
|
||||||
// Clear seek state after Android seek
|
// Clear seek state after Android seek
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
|
|
@ -400,12 +465,18 @@ const VideoPlayer: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
// iOS fallback - use seek prop
|
// iOS (and other platforms) – prefer direct seek on the ref to avoid re-mounts caused by the `seek` prop
|
||||||
const position = timeInSeconds / duration;
|
const position = timeInSeconds / duration; // VLC expects a 0-1 fraction
|
||||||
setSeekPosition(position);
|
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(() => {
|
setTimeout(() => {
|
||||||
if (isMounted.current) {
|
if (isMounted.current) {
|
||||||
|
// Reset temporary seek state
|
||||||
setSeekPosition(null);
|
setSeekPosition(null);
|
||||||
isSeeking.current = false;
|
isSeeking.current = false;
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
|
|
@ -967,6 +1038,25 @@ const VideoPlayer: React.FC = () => {
|
||||||
]}
|
]}
|
||||||
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
|
pointerEvents={isOpeningAnimationComplete ? 'none' : 'auto'}
|
||||||
>
|
>
|
||||||
|
{backdrop && (
|
||||||
|
<Image
|
||||||
|
source={{ uri: backdrop }}
|
||||||
|
style={[StyleSheet.absoluteFill, { width: screenDimensions.width, height: screenDimensions.height }]}
|
||||||
|
resizeMode="cover"
|
||||||
|
blurRadius={0}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<LinearGradient
|
||||||
|
colors={[
|
||||||
|
'rgba(0,0,0,0.3)',
|
||||||
|
'rgba(0,0,0,0.6)',
|
||||||
|
'rgba(0,0,0,0.8)',
|
||||||
|
'rgba(0,0,0,0.9)'
|
||||||
|
]}
|
||||||
|
locations={[0, 0.3, 0.7, 1]}
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
/>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.loadingCloseButton}
|
style={styles.loadingCloseButton}
|
||||||
onPress={handleClose}
|
onPress={handleClose}
|
||||||
|
|
@ -976,8 +1066,28 @@ const VideoPlayer: React.FC = () => {
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.openingContent}>
|
<View style={styles.openingContent}>
|
||||||
<ActivityIndicator size="large" color="#E50914" />
|
{hasLogo ? (
|
||||||
<Text style={styles.openingText}>Loading video...</Text>
|
<Animated.View style={{
|
||||||
|
transform: [
|
||||||
|
{ scale: Animated.multiply(logoScaleAnim, pulseAnim) }
|
||||||
|
],
|
||||||
|
opacity: logoOpacityAnim,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: metadata.logo }}
|
||||||
|
style={{
|
||||||
|
width: 300,
|
||||||
|
height: 180,
|
||||||
|
resizeMode: 'contain',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ActivityIndicator size="large" color="#E50914" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
|
@ -1055,7 +1165,6 @@ const VideoPlayer: React.FC = () => {
|
||||||
resizeMode={resizeMode as any}
|
resizeMode={resizeMode as any}
|
||||||
audioTrack={selectedAudioTrack ?? undefined}
|
audioTrack={selectedAudioTrack ?? undefined}
|
||||||
textTrack={useCustomSubtitles ? -1 : (selectedTextTrack ?? undefined)}
|
textTrack={useCustomSubtitles ? -1 : (selectedTextTrack ?? undefined)}
|
||||||
seek={Platform.OS === 'ios' ? (seekPosition ?? undefined) : undefined}
|
|
||||||
autoAspectRatio
|
autoAspectRatio
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
|
||||||
|
|
@ -474,7 +474,6 @@ export const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
openingContent: {
|
openingContent: {
|
||||||
padding: 20,
|
padding: 20,
|
||||||
backgroundColor: 'rgba(0,0,0,0.85)',
|
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ export type RootStackParamList = {
|
||||||
type: string;
|
type: string;
|
||||||
stream: Stream;
|
stream: Stream;
|
||||||
episodeId?: string;
|
episodeId?: string;
|
||||||
|
backdrop?: string;
|
||||||
};
|
};
|
||||||
Player: {
|
Player: {
|
||||||
uri: string;
|
uri: string;
|
||||||
|
|
@ -82,6 +83,7 @@ export type RootStackParamList = {
|
||||||
episodeId?: string;
|
episodeId?: string;
|
||||||
imdbId?: string;
|
imdbId?: string;
|
||||||
availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } };
|
availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } };
|
||||||
|
backdrop?: string;
|
||||||
};
|
};
|
||||||
Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string };
|
Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string };
|
||||||
Credits: { mediaId: string; mediaType: string };
|
Credits: { mediaId: string; mediaType: string };
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import { RootStackParamList, RootStackNavigationProp } from '../navigation/AppNavigator';
|
import { RootStackParamList, RootStackNavigationProp } from '../navigation/AppNavigator';
|
||||||
import { useMetadata } from '../hooks/useMetadata';
|
import { useMetadata } from '../hooks/useMetadata';
|
||||||
|
import { useMetadataAssets } from '../hooks/useMetadataAssets';
|
||||||
import { useTheme } from '../contexts/ThemeContext';
|
import { useTheme } from '../contexts/ThemeContext';
|
||||||
import { Stream } from '../types/metadata';
|
import { Stream } from '../types/metadata';
|
||||||
import { tmdbService } from '../services/tmdbService';
|
import { tmdbService } from '../services/tmdbService';
|
||||||
|
|
@ -274,6 +275,11 @@ export const StreamsScreen = () => {
|
||||||
imdbId,
|
imdbId,
|
||||||
} = useMetadata({ id, type });
|
} = 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
|
// Create styles using current theme colors
|
||||||
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||||
|
|
||||||
|
|
@ -578,6 +584,7 @@ export const StreamsScreen = () => {
|
||||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
|
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
|
||||||
imdbId: imdbId || undefined,
|
imdbId: imdbId || undefined,
|
||||||
availableStreams: streamsToPass,
|
availableStreams: streamsToPass,
|
||||||
|
backdrop: bannerImage || undefined,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[StreamsScreen] Error locking orientation before navigation:', error);
|
logger.error('[StreamsScreen] Error locking orientation before navigation:', error);
|
||||||
|
|
@ -598,9 +605,10 @@ export const StreamsScreen = () => {
|
||||||
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
|
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined,
|
||||||
imdbId: imdbId || undefined,
|
imdbId: imdbId || undefined,
|
||||||
availableStreams: streamsToPass,
|
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
|
// Update handleStreamPress
|
||||||
const handleStreamPress = useCallback(async (stream: Stream) => {
|
const handleStreamPress = useCallback(async (stream: Stream) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue