diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 3ea80d7..1ad89c0 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -29,6 +29,7 @@ import AudioTrackModal from './modals/AudioTrackModal'; import ResumeOverlay from './modals/ResumeOverlay'; import PlayerControls from './controls/PlayerControls'; import CustomSubtitles from './subtitles/CustomSubtitles'; +import SourcesModal from './modals/SourcesModal'; const VideoPlayer: React.FC = () => { const navigation = useNavigation(); @@ -46,7 +47,8 @@ const VideoPlayer: React.FC = () => { id, type, episodeId, - imdbId + imdbId, + availableStreams: passedAvailableStreams } = route.params; safeDebugLog("Component mounted with props", { @@ -112,7 +114,15 @@ const VideoPlayer: React.FC = () => { const [availableSubtitles, setAvailableSubtitles] = useState([]); const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false); const [isLoadingSubtitleList, setIsLoadingSubtitleList] = useState(false); - const isMounted = useRef(true); + const [showSourcesModal, setShowSourcesModal] = useState(false); + const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: any[]; addonName: string } }>(passedAvailableStreams || {}); + const [currentStreamUrl, setCurrentStreamUrl] = useState(uri); + const [isChangingSource, setIsChangingSource] = useState(false); + const [pendingSeek, setPendingSeek] = useState<{ position: number; shouldPlay: boolean } | null>(null); + const [currentQuality, setCurrentQuality] = useState(quality); + const [currentStreamProvider, setCurrentStreamProvider] = useState(streamProvider); + const [currentStreamName, setCurrentStreamName] = useState(undefined); + const isMounted = useRef(true); const calculateVideoStyles = (videoWidth: number, videoHeight: number, screenWidth: number, screenHeight: number) => { return { @@ -728,6 +738,123 @@ const VideoPlayer: React.FC = () => { saveSubtitleSize(newSize); }; + useEffect(() => { + if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) { + logger.log(`[VideoPlayer] Player ready after source change, seeking to position: ${pendingSeek.position}s out of ${duration}s total`); + + if (pendingSeek.position > 0 && vlcRef.current) { + // Wait longer for the player to be fully ready and stable + setTimeout(() => { + if (vlcRef.current && duration > 0 && pendingSeek) { + logger.log(`[VideoPlayer] Executing seek to ${pendingSeek.position}s`); + + // Use our existing seekToTime function which handles VLC methods properly + seekToTime(pendingSeek.position); + + // Also update the current time state to reflect the seek + setCurrentTime(pendingSeek.position); + + // Resume playback if it was playing before the source change + if (pendingSeek.shouldPlay) { + setTimeout(() => { + logger.log('[VideoPlayer] Resuming playback after seek'); + setPaused(false); + if (vlcRef.current && typeof vlcRef.current.play === 'function') { + vlcRef.current.play(); + } + }, 700); // Wait longer for seek to complete properly + } + + // Clean up after a reasonable delay + setTimeout(() => { + setPendingSeek(null); + setIsChangingSource(false); + }, 800); + } + }, 1500); // Increased delay to ensure player is fully stable + } else { + // No seeking needed, just resume playback if it was playing + if (pendingSeek.shouldPlay) { + setTimeout(() => { + logger.log('[VideoPlayer] No seek needed, just resuming playback'); + setPaused(false); + if (vlcRef.current && typeof vlcRef.current.play === 'function') { + vlcRef.current.play(); + } + }, 500); + } + + setTimeout(() => { + setPendingSeek(null); + setIsChangingSource(false); + }, 600); + } + } + }, [pendingSeek, isPlayerReady, isVideoLoaded, duration]); + + const handleSelectStream = async (newStream: any) => { + if (newStream.url === currentStreamUrl) { + setShowSourcesModal(false); + return; + } + + setIsChangingSource(true); + setShowSourcesModal(false); + + try { + // Save current state + const savedPosition = currentTime; + const wasPlaying = !paused; + + logger.log(`[VideoPlayer] Changing source from ${currentStreamUrl} to ${newStream.url}`); + logger.log(`[VideoPlayer] Saved position: ${savedPosition}, was playing: ${wasPlaying}`); + + // Extract quality and provider information from the new stream + let newQuality = newStream.quality; + if (!newQuality && newStream.title) { + // Try to extract quality from title (e.g., "1080p", "720p") + const qualityMatch = newStream.title.match(/(\d+)p/); + newQuality = qualityMatch ? qualityMatch[0] : undefined; // Use [0] to get full match like "1080p" + } + + // For provider, try multiple fields + const newProvider = newStream.addonName || newStream.name || newStream.addon || 'Unknown'; + + // For stream name, prioritize the stream name over title + const newStreamName = newStream.name || newStream.title || 'Unknown Stream'; + + logger.log(`[VideoPlayer] Stream object:`, newStream); + logger.log(`[VideoPlayer] Extracted - Quality: ${newQuality}, Provider: ${newProvider}, Stream Name: ${newStreamName}`); + logger.log(`[VideoPlayer] Available fields - quality: ${newStream.quality}, title: ${newStream.title}, addonName: ${newStream.addonName}, name: ${newStream.name}, addon: ${newStream.addon}`); + + // Stop current playback + if (vlcRef.current) { + vlcRef.current.pause && vlcRef.current.pause(); + } + setPaused(true); + + // Set pending seek state + setPendingSeek({ position: savedPosition, shouldPlay: wasPlaying }); + + // Update the stream URL and details immediately + setCurrentStreamUrl(newStream.url); + setCurrentQuality(newQuality); + setCurrentStreamProvider(newProvider); + setCurrentStreamName(newStreamName); + + // Reset player state for new source + setCurrentTime(0); + setDuration(0); + setIsPlayerReady(false); + setIsVideoLoaded(false); + + } catch (error) { + logger.error('[VideoPlayer] Error changing source:', error); + setPendingSeek(null); + setIsChangingSource(false); + } + }; + return ( { + {/* Source Change Loading Overlay */} + {isChangingSource && ( + + + + Changing source... + Please wait while we load the new stream + + + )} + { ], }} source={{ - uri: uri, + uri: currentStreamUrl, initOptions: [ '--rtsp-tcp', '--network-caching=150', @@ -849,21 +997,23 @@ const VideoPlayer: React.FC = () => { episodeTitle={episodeTitle} season={season} episode={episode} - quality={quality} + quality={currentQuality || quality} year={year} - streamProvider={streamProvider} + streamProvider={currentStreamProvider || streamProvider} currentTime={currentTime} duration={duration} playbackSpeed={playbackSpeed} zoomScale={zoomScale} vlcAudioTracks={vlcAudioTracks} selectedAudioTrack={selectedAudioTrack} + availableStreams={availableStreams} togglePlayback={togglePlayback} skip={skip} handleClose={handleClose} cycleAspectRatio={cycleAspectRatio} setShowAudioModal={setShowAudioModal} setShowSubtitleModal={setShowSubtitleModal} + setShowSourcesModal={setShowSourcesModal} progressBarRef={progressBarRef} progressAnim={progressAnim} handleProgressBarTouch={handleProgressBarTouch} @@ -923,6 +1073,15 @@ const VideoPlayer: React.FC = () => { increaseSubtitleSize={increaseSubtitleSize} decreaseSubtitleSize={decreaseSubtitleSize} /> + + ); }; diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index cc73fb5..47fe022 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -22,12 +22,14 @@ interface PlayerControlsProps { zoomScale: number; vlcAudioTracks: Array<{id: number, name: string, language?: string}>; selectedAudioTrack: number | null; + availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; togglePlayback: () => void; skip: (seconds: number) => void; handleClose: () => void; cycleAspectRatio: () => void; setShowAudioModal: (show: boolean) => void; setShowSubtitleModal: (show: boolean) => void; + setShowSourcesModal?: (show: boolean) => void; progressBarRef: React.RefObject; progressAnim: Animated.Value; handleProgressBarTouch: (event: any) => void; @@ -55,12 +57,14 @@ export const PlayerControls: React.FC = ({ zoomScale, vlcAudioTracks, selectedAudioTrack, + availableStreams, togglePlayback, skip, handleClose, cycleAspectRatio, setShowAudioModal, setShowSubtitleModal, + setShowSourcesModal, progressBarRef, progressAnim, handleProgressBarTouch, @@ -206,6 +210,19 @@ export const PlayerControls: React.FC = ({ Subtitles + + {/* Change Source Button */} + {setShowSourcesModal && ( + setShowSourcesModal(true)} + > + + + Change Source + + + )} diff --git a/src/components/player/modals/SourcesModal.tsx b/src/components/player/modals/SourcesModal.tsx new file mode 100644 index 0000000..ad5bc99 --- /dev/null +++ b/src/components/player/modals/SourcesModal.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native'; +import { Ionicons, MaterialIcons } from '@expo/vector-icons'; +import { styles } from '../utils/playerStyles'; +import { Stream } from '../../../types/streams'; +import QualityBadge from '../../metadata/QualityBadge'; + +interface SourcesModalProps { + showSourcesModal: boolean; + setShowSourcesModal: (show: boolean) => void; + availableStreams: { [providerId: string]: { streams: Stream[]; addonName: string } }; + currentStreamUrl: string; + onSelectStream: (stream: Stream) => void; + isChangingSource: boolean; +} + +const SourcesModal: React.FC = ({ + showSourcesModal, + setShowSourcesModal, + availableStreams, + currentStreamUrl, + onSelectStream, + isChangingSource, +}) => { + if (!showSourcesModal) return null; + + const sortedProviders = Object.entries(availableStreams).sort(([a], [b]) => { + // Put HDRezka first + if (a === 'hdrezka') return -1; + if (b === 'hdrezka') return 1; + return 0; + }); + + const handleStreamSelect = (stream: Stream) => { + if (stream.url !== currentStreamUrl && !isChangingSource) { + onSelectStream(stream); + } + }; + + const getQualityFromTitle = (title?: string): string | null => { + if (!title) return null; + const match = title.match(/(\d+)p/); + return match ? match[1] : null; + }; + + const isStreamSelected = (stream: Stream): boolean => { + return stream.url === currentStreamUrl; + }; + + return ( + + + + Choose Source + setShowSourcesModal(false)} + > + + + + + + {sortedProviders.map(([providerId, { streams, addonName }]) => ( + + {addonName} + + {streams.map((stream, index) => { + const quality = getQualityFromTitle(stream.title); + const isSelected = isStreamSelected(stream); + const isHDR = stream.title?.toLowerCase().includes('hdr'); + const isDolby = stream.title?.toLowerCase().includes('dolby') || stream.title?.includes('DV'); + const size = stream.title?.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1]; + const isDebrid = stream.behaviorHints?.cached; + const isHDRezka = providerId === 'hdrezka'; + + return ( + handleStreamSelect(stream)} + disabled={isChangingSource || isSelected} + activeOpacity={0.7} + > + + + + {isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream')} + + + {isSelected && ( + + + Current + + )} + + {isChangingSource && isSelected && ( + + )} + + + {!isHDRezka && stream.title && stream.title !== stream.name && ( + {stream.title} + )} + + + {quality && quality >= "720" && ( + + )} + + {isDolby && ( + + )} + + {size && ( + + {size} + + )} + + {isDebrid && ( + + DEBRID + + )} + + {isHDRezka && ( + + HDREZKA + + )} + + + + + {isSelected ? ( + + ) : ( + + )} + + + ); + })} + + ))} + + + + ); +}; + +export default SourcesModal; \ No newline at end of file diff --git a/src/components/player/utils/playerStyles.ts b/src/components/player/utils/playerStyles.ts index e2834e2..84cea74 100644 --- a/src/components/player/utils/playerStyles.ts +++ b/src/components/player/utils/playerStyles.ts @@ -76,15 +76,16 @@ export const styles = StyleSheet.create({ marginRight: 8, }, qualityBadge: { - backgroundColor: '#E50914', - paddingHorizontal: 6, + backgroundColor: 'rgba(229, 9, 20, 0.2)', + paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, marginRight: 8, + marginBottom: 4, }, qualityText: { - color: 'white', - fontSize: 10, + color: '#E50914', + fontSize: 11, fontWeight: 'bold', }, providerText: { @@ -189,9 +190,10 @@ export const styles = StyleSheet.create({ }, modalOverlay: { flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.9)', justifyContent: 'center', alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.7)', + padding: 20, }, modalContent: { width: '80%', @@ -207,16 +209,15 @@ export const styles = StyleSheet.create({ shadowRadius: 5, }, modalHeader: { - padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - borderBottomWidth: 1, - borderBottomColor: '#333', + marginBottom: 20, + paddingHorizontal: 4, }, modalTitle: { color: 'white', - fontSize: 18, + fontSize: 20, fontWeight: 'bold', }, trackList: { @@ -764,4 +765,223 @@ export const styles = StyleSheet.create({ alignItems: 'center', zIndex: 9999, }, + // Sources Modal Styles + sourcesModal: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.9)', + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + sourcesContainer: { + backgroundColor: 'rgba(20, 20, 20, 0.98)', + borderRadius: 12, + width: '100%', + maxWidth: 500, + maxHeight: '80%', + paddingVertical: 20, + paddingHorizontal: 16, + }, + sourcesHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 20, + paddingHorizontal: 4, + }, + sourcesTitle: { + color: 'white', + fontSize: 20, + fontWeight: 'bold', + }, + modalCloseButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + justifyContent: 'center', + alignItems: 'center', + }, + sourcesScrollView: { + maxHeight: 400, + }, + sourceItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 16, + paddingHorizontal: 12, + borderRadius: 8, + backgroundColor: 'rgba(255, 255, 255, 0.05)', + marginBottom: 8, + }, + currentSourceItem: { + backgroundColor: 'rgba(229, 9, 20, 0.2)', + borderWidth: 1, + borderColor: 'rgba(229, 9, 20, 0.5)', + }, + sourceInfo: { + flex: 1, + marginLeft: 12, + }, + sourceTitle: { + color: 'white', + fontSize: 16, + fontWeight: '600', + marginBottom: 4, + }, + sourceDetails: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + }, + sourceDetailText: { + color: '#888', + fontSize: 12, + marginRight: 8, + marginBottom: 4, + }, + currentStreamBadge: { + backgroundColor: 'rgba(0, 255, 0, 0.2)', + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + marginRight: 8, + marginBottom: 4, + }, + currentStreamText: { + color: '#00FF00', + fontSize: 11, + fontWeight: 'bold', + }, + switchingSourceOverlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 9999, + }, + switchingContent: { + alignItems: 'center', + backgroundColor: 'rgba(20, 20, 20, 0.9)', + padding: 30, + borderRadius: 12, + minWidth: 200, + }, + switchingText: { + color: 'white', + fontSize: 16, + fontWeight: '600', + marginTop: 12, + textAlign: 'center', + }, + // Additional SourcesModal styles + sourceProviderSection: { + marginBottom: 20, + }, + sourceProviderTitle: { + color: 'rgba(255, 255, 255, 0.8)', + fontSize: 14, + fontWeight: '600', + marginBottom: 12, + paddingHorizontal: 4, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + sourceStreamItem: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + paddingHorizontal: 12, + borderRadius: 8, + backgroundColor: 'rgba(255, 255, 255, 0.05)', + marginBottom: 8, + }, + sourceStreamItemSelected: { + backgroundColor: 'rgba(229, 9, 20, 0.2)', + borderWidth: 1, + borderColor: 'rgba(229, 9, 20, 0.5)', + }, + sourceStreamDetails: { + flex: 1, + }, + sourceStreamTitleRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 4, + }, + sourceStreamTitle: { + color: 'white', + fontSize: 16, + fontWeight: '600', + flex: 1, + }, + sourceStreamTitleSelected: { + color: '#E50914', + }, + sourceStreamSubtitle: { + color: 'rgba(255, 255, 255, 0.7)', + fontSize: 14, + marginBottom: 6, + }, + sourceStreamMeta: { + flexDirection: 'row', + alignItems: 'center', + flexWrap: 'wrap', + }, + sourceChip: { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + marginRight: 6, + marginBottom: 4, + }, + sourceChipText: { + color: 'rgba(255, 255, 255, 0.8)', + fontSize: 11, + fontWeight: 'bold', + }, + debridChip: { + backgroundColor: 'rgba(0, 255, 0, 0.2)', + }, + hdrezkaChip: { + backgroundColor: 'rgba(255, 165, 0, 0.2)', + }, + sourceStreamAction: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + justifyContent: 'center', + alignItems: 'center', + }, + // Source Change Loading Overlay + sourceChangeOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.9)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 5000, + }, + sourceChangeContent: { + alignItems: 'center', + padding: 30, + }, + sourceChangeText: { + color: '#E50914', + fontSize: 18, + fontWeight: 'bold', + marginTop: 15, + textAlign: 'center', + }, + sourceChangeSubtext: { + color: 'rgba(255, 255, 255, 0.8)', + fontSize: 14, + marginTop: 8, + textAlign: 'center', + }, }); \ No newline at end of file diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index e25b1d6..aa63dce 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -35,7 +35,6 @@ export interface AppSettings { logoSourcePreference: 'metahub' | 'tmdb'; // Preferred source for title logos tmdbLanguagePreference: string; // Preferred language for TMDB logos (ISO 639-1 code) enableInternalProviders: boolean; // Toggle for internal providers like HDRezka - autoPlayFirstStream: boolean; // Auto-play first stream without showing streams selection } export const DEFAULT_SETTINGS: AppSettings = { @@ -53,7 +52,6 @@ export const DEFAULT_SETTINGS: AppSettings = { logoSourcePreference: 'metahub', // Default to Metahub as first source tmdbLanguagePreference: 'en', // Default to English enableInternalProviders: true, // Enable internal providers by default - autoPlayFirstStream: false, // Default to false to maintain existing behavior }; const SETTINGS_STORAGE_KEY = 'app_settings'; diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index c5498d0..dab0094 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -78,6 +78,7 @@ export type RootStackParamList = { type?: string; episodeId?: string; imdbId?: string; + availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; }; Catalog: { id: string; type: string; addonId?: string; name?: string; genreFilter?: string }; Credits: { mediaId: string; mediaType: string }; diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index e5d9252..29a690b 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -12,7 +12,6 @@ import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' import { useRoute, useNavigation } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; -import * as ScreenOrientation from 'expo-screen-orientation'; import { useTheme } from '../contexts/ThemeContext'; import { useMetadata } from '../hooks/useMetadata'; import { CastSection } from '../components/metadata/CastSection'; @@ -21,7 +20,6 @@ import { MovieContent } from '../components/metadata/MovieContent'; import { MoreLikeThisSection } from '../components/metadata/MoreLikeThisSection'; import { RatingsSection } from '../components/metadata/RatingsSection'; import { RouteParams, Episode } from '../types/metadata'; -import { Stream, GroupedStreams } from '../types/streams'; import Animated, { useAnimatedStyle, interpolate, @@ -77,10 +75,6 @@ const MetadataScreen: React.FC = () => { loadingRecommendations, setMetadata, imdbId, - loadStreams, - loadEpisodeStreams, - groupedStreams, - episodeStreams, } = useMetadata({ id, type }); // Optimized hooks with memoization @@ -113,127 +107,8 @@ const MetadataScreen: React.FC = () => { handleSeasonChange(seasonNumber); }, [handleSeasonChange]); - // Helper function to get the first available stream from grouped streams - const getFirstAvailableStream = useCallback((streams: GroupedStreams): Stream | null => { - const providers = Object.values(streams); - for (const provider of providers) { - if (provider.streams && provider.streams.length > 0) { - // Try to find a cached stream first - const cachedStream = provider.streams.find(stream => - stream.behaviorHints?.cached === true - ); - if (cachedStream) { - return cachedStream; - } - - // Otherwise return the first stream - return provider.streams[0]; - } - } - return null; - }, []); - - const handleShowStreams = useCallback(async () => { + const handleShowStreams = useCallback(() => { const { watchProgress } = watchProgressData; - - // Check if auto-play is enabled - if (settings.autoPlayFirstStream) { - try { - console.log('Auto-play enabled, attempting to load streams...'); - - // Determine the target episode for series - let targetEpisodeId: string | undefined; - if (type === 'series') { - targetEpisodeId = watchProgress?.episodeId || episodeId || (episodes.length > 0 ? - (episodes[0].stremioId || `${id}:${episodes[0].season_number}:${episodes[0].episode_number}`) : undefined); - } - - // Load streams without locking orientation yet - let streamsLoaded = false; - if (type === 'series' && targetEpisodeId) { - console.log('Loading episode streams for:', targetEpisodeId); - await loadEpisodeStreams(targetEpisodeId); - streamsLoaded = true; - } else if (type === 'movie') { - console.log('Loading movie streams...'); - await loadStreams(); - streamsLoaded = true; - } - - if (streamsLoaded) { - // Wait a bit longer for streams to be processed and state to update - console.log('Waiting for streams to be processed...'); - await new Promise(resolve => setTimeout(resolve, 3000)); - - // Check if we have any streams available - const availableStreams = type === 'series' ? episodeStreams : groupedStreams; - console.log('Available streams:', Object.keys(availableStreams)); - - const firstStream = getFirstAvailableStream(availableStreams); - - if (firstStream) { - console.log('Found stream, navigating to player:', firstStream); - - // Now lock orientation to landscape before navigation - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); - await new Promise(resolve => setTimeout(resolve, 200)); - - if (type === 'series' && targetEpisodeId) { - // Get episode details for navigation - const targetEpisode = episodes.find(ep => - ep.stremioId === targetEpisodeId || - `${id}:${ep.season_number}:${ep.episode_number}` === targetEpisodeId - ); - - // Navigate directly to player with the first stream - navigation.navigate('Player', { - uri: firstStream.url, - title: metadata?.name || 'Unknown', - season: targetEpisode?.season_number, - episode: targetEpisode?.episode_number, - episodeTitle: targetEpisode?.name, - quality: firstStream.title?.match(/(\d+)p/)?.[1] || 'Unknown', - year: metadata?.year, - streamProvider: firstStream.name || 'Unknown', - id, - type, - episodeId: targetEpisodeId, - imdbId: imdbId || id, - }); - return; - } else if (type === 'movie') { - // Navigate directly to player with the first stream - navigation.navigate('Player', { - uri: firstStream.url, - title: metadata?.name || 'Unknown', - quality: firstStream.title?.match(/(\d+)p/)?.[1] || 'Unknown', - year: metadata?.year, - streamProvider: firstStream.name || 'Unknown', - id, - type, - imdbId: imdbId || id, - }); - return; - } - } else { - console.log('No streams found after waiting, disabling auto-play for this session'); - // Don't fall back to streams screen, just show an alert - alert('No streams available for auto-play. Please try selecting streams manually.'); - return; - } - } - - console.log('Auto-play failed, falling back to manual selection'); - } catch (error) { - console.error('Auto-play failed with error:', error); - // Don't fall back on error, just show alert - alert('Auto-play failed. Please try selecting streams manually.'); - return; - } - } - - // Normal behavior: navigate to streams screen (only if auto-play is disabled or not attempted) - console.log('Navigating to streams screen (normal flow)'); if (type === 'series') { const targetEpisodeId = watchProgress?.episodeId || episodeId || (episodes.length > 0 ? (episodes[0].stremioId || `${id}:${episodes[0].season_number}:${episodes[0].episode_number}`) : undefined); @@ -244,7 +119,7 @@ const MetadataScreen: React.FC = () => { } } navigation.navigate('Streams', { id, type, episodeId }); - }, [settings.autoPlayFirstStream, navigation, id, type, episodes, episodeId, watchProgressData, metadata, loadEpisodeStreams, loadStreams, episodeStreams, groupedStreams, imdbId, getFirstAvailableStream]); + }, [navigation, id, type, episodes, episodeId, watchProgressData.watchProgress]); const handleEpisodeSelect = useCallback((episode: Episode) => { const episodeId = episode.stremioId || `${id}:${episode.season_number}:${episode.episode_number}`; diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index d4bd7c9..9dbe102 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -462,17 +462,6 @@ const SettingsScreen: React.FC = () => { icon="play-arrow" renderControl={ChevronRight} onPress={() => navigation.navigate('PlayerSettings')} - /> - ( - updateSetting('autoPlayFirstStream', value)} - /> - )} isLast={true} /> diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 5ed5407..ea726d4 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -519,6 +519,9 @@ export const StreamsScreen = () => { // Small delay to ensure orientation is set before navigation await new Promise(resolve => setTimeout(resolve, 100)); + // Prepare available streams for the change source feature + const streamsToPass = type === 'series' ? episodeStreams : groupedStreams; + navigation.navigate('Player', { uri: stream.url, title: metadata?.name || '', @@ -532,10 +535,13 @@ export const StreamsScreen = () => { type, episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, imdbId: imdbId || undefined, + availableStreams: streamsToPass, }); } catch (error) { logger.error('[StreamsScreen] Error locking orientation before navigation:', error); // Fallback: navigate anyway + const streamsToPass = type === 'series' ? episodeStreams : groupedStreams; + navigation.navigate('Player', { uri: stream.url, title: metadata?.name || '', @@ -549,9 +555,10 @@ export const StreamsScreen = () => { type, episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined, imdbId: imdbId || undefined, + availableStreams: streamsToPass, }); } - }, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId]); + }, [metadata, type, currentEpisode, navigation, id, selectedEpisode, imdbId, episodeStreams, groupedStreams]); // Update handleStreamPress const handleStreamPress = useCallback(async (stream: Stream) => {