diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx
index 10ebf277..22256bd7 100644
--- a/src/screens/StreamsScreen.tsx
+++ b/src/screens/StreamsScreen.tsx
@@ -517,6 +517,19 @@ export const StreamsScreen = () => {
const { pauseTrailer, resumeTrailer } = useTrailer();
const { showSuccess, showInfo } = useToast();
+ // Add dimension listener and tablet detection
+ const [dimensions, setDimensions] = useState(Dimensions.get('window'));
+
+ useEffect(() => {
+ const subscription = Dimensions.addEventListener('change', ({ window }) => {
+ setDimensions(window);
+ });
+ return () => subscription?.remove();
+ }, []);
+
+ const deviceWidth = dimensions.width;
+ const isTablet = deviceWidth >= 768;
+
// Add refs to prevent excessive updates and duplicate loads
const isMounted = useRef(true);
const loadStartTimeRef = useRef(0);
@@ -2012,7 +2025,7 @@ export const StreamsScreen = () => {
{Platform.OS !== 'ios' && (
{
)}
- {type === 'movie' && metadata && (
-
-
- {metadata.logo && !movieLogoError ? (
- setMovieLogoError(true)}
- />
- ) : (
-
- {metadata.name}
-
- )}
-
-
- )}
-
- {metadata?.videos && metadata.videos.length > 1 && selectedEpisode && (
-
-
-
+ {/* Left Panel: Thumbnail Background */}
+
+
+
-
-
-
- {currentEpisode ? (
-
-
- {currentEpisode.episodeString}
-
-
- {currentEpisode.name}
-
- {!!currentEpisode.overview && (
-
-
- {currentEpisode.overview}
-
-
- )}
-
-
- {tmdbService.formatAirDate(currentEpisode.air_date)}
-
- {effectiveEpisodeVote > 0 && (
-
-
-
- {effectiveEpisodeVote.toFixed(1)}
-
-
- )}
- {!!effectiveEpisodeRuntime && (
-
-
-
- {effectiveEpisodeRuntime >= 60
- ? `${Math.floor(effectiveEpisodeRuntime / 60)}h ${effectiveEpisodeRuntime % 60}m`
- : `${effectiveEpisodeRuntime}m`}
-
-
- )}
-
-
+ {type === 'movie' && metadata && (
+
+ {metadata.logo && !movieLogoError ? (
+ setMovieLogoError(true)}
+ />
) : (
- // Placeholder to reserve space and avoid layout shift while loading
-
+ {metadata.name}
)}
-
+ )}
+
+ {type === 'series' && currentEpisode && (
+
+ {currentEpisode.episodeString}
+ {currentEpisode.name}
+ {currentEpisode.overview && (
+ {currentEpisode.overview}
+ )}
+
+ )}
+
+
+
+ {/* Right Panel: Streams List */}
+
+
+
+ {!streamsEmpty && (
+
+ )}
+
+
+ {/* Active Scrapers Status */}
+ {activeFetchingScrapers.length > 0 && (
+
+ Fetching from:
+
+ {activeFetchingScrapers.map((scraperName, index) => (
+
+ ))}
+
+
+ )}
+
+ {/* Update the streams/loading state display logic */}
+ { showNoSourcesError ? (
+
+
+ No streaming sources available
+
+ Please add streaming sources in settings
+
+ navigation.navigate('Addons')}
+ >
+ Add Sources
+
+
+ ) : streamsEmpty ? (
+ showInitialLoading ? (
+
+
+
+ {isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
+
+
+ ) : showStillFetching ? (
+
+
+ Still fetching streams…
+
+ ) : (
+ // No streams and not loading = no streams available
+
+
+ No streams available
+
+ )
+ ) : (
+ // Show streams immediately when available, even if still loading others
+
+ {/* Show autoplay loading overlay if waiting for autoplay */}
+ {isAutoplayWaiting && !autoplayTriggered && (
+
+
+
+ Starting best stream...
+
+
+ )}
+
+
+ {sections.filter(Boolean).map((section, sectionIndex) => (
+
+ {/* Section Header */}
+ {renderSectionHeader({ section: section! })}
+
+ {/* Stream Cards using FlatList */}
+ {section!.data && section!.data.length > 0 ? (
+ {
+ if (item && item.url) {
+ return `${item.url}-${sectionIndex}-${index}`;
+ }
+ return `empty-${sectionIndex}-${index}`;
+ }}
+ renderItem={({ item, index }) => (
+
+ handleStreamPress(item)}
+ index={index}
+ isLoading={false}
+ statusMessage={undefined}
+ theme={currentTheme}
+ showLogos={settings.showScraperLogos}
+ scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogoCache.get((item.addonId || (item as any).addon) as string) || null : null}
+ showAlert={(t, m) => openAlert(t, m)}
+ parentTitle={metadata?.name}
+ parentType={type as 'movie' | 'series'}
+ parentSeason={(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined}
+ parentEpisode={(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined}
+ parentEpisodeTitle={(type === 'series' || type === 'other') ? currentEpisode?.name : undefined}
+ parentPosterUrl={episodeImage || metadata?.poster || undefined}
+ providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))}
+ parentId={id}
+ parentImdbId={imdbId || undefined}
+ />
+
+ )}
+ scrollEnabled={false}
+ initialNumToRender={6}
+ maxToRenderPerBatch={2}
+ windowSize={3}
+ removeClippedSubviews={true}
+ showsVerticalScrollIndicator={false}
+ getItemLayout={(data, index) => ({
+ length: 78, // Approximate height of StreamCard (68 minHeight + 10 marginBottom)
+ offset: 78 * index,
+ index,
+ })}
+ />
+ ) : null}
+
+ ))}
+
+ {/* Footer Loading */}
+ {(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
+
+
+ Loading more sources...
+
+ )}
+
+
+ )}
- )}
-
-
-
- {!streamsEmpty && (
-
+ ) : (
+ // PHONE LAYOUT (existing structure)
+ <>
+ {type === 'movie' && metadata && (
+
+
+ {metadata.logo && !movieLogoError ? (
+ setMovieLogoError(true)}
+ />
+ ) : (
+
+ {metadata.name}
+
+ )}
+
+
)}
-
- {/* Active Scrapers Status */}
- {activeFetchingScrapers.length > 0 && (
-
- Fetching from:
-
- {activeFetchingScrapers.map((scraperName, index) => (
-
- ))}
+ {metadata?.videos && metadata.videos.length > 1 && selectedEpisode && (
+
+
+
+
+
+
+ {currentEpisode ? (
+
+
+ {currentEpisode.episodeString}
+
+
+ {currentEpisode.name}
+
+ {!!currentEpisode.overview && (
+
+
+ {currentEpisode.overview}
+
+
+ )}
+
+
+ {tmdbService.formatAirDate(currentEpisode.air_date)}
+
+ {effectiveEpisodeVote > 0 && (
+
+
+
+ {effectiveEpisodeVote.toFixed(1)}
+
+
+ )}
+ {!!effectiveEpisodeRuntime && (
+
+
+
+ {effectiveEpisodeRuntime >= 60
+ ? `${Math.floor(effectiveEpisodeRuntime / 60)}h ${effectiveEpisodeRuntime % 60}m`
+ : `${effectiveEpisodeRuntime}m`}
+
+
+ )}
+
+
+ ) : (
+ // Placeholder to reserve space and avoid layout shift while loading
+
+ )}
+
+
+
+
-
- )}
+ )}
- {/* Update the streams/loading state display logic */}
- { showNoSourcesError ? (
-
-
- No streaming sources available
-
- Please add streaming sources in settings
-
- navigation.navigate('Addons')}
- >
- Add Sources
-
+
+
+ {!streamsEmpty && (
+
+ )}
- ) : streamsEmpty ? (
- showInitialLoading ? (
-
-
-
- {isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
-
-
- ) : showStillFetching ? (
-
-
- Still fetching streams…
-
- ) : (
- // No streams and not loading = no streams available
-
-
- No streams available
-
- )
- ) : (
- // Show streams immediately when available, even if still loading others
-
- {/* Show autoplay loading overlay if waiting for autoplay */}
- {isAutoplayWaiting && !autoplayTriggered && (
+
+ {/* Active Scrapers Status */}
+ {activeFetchingScrapers.length > 0 && (
-
-
- Starting best stream...
+ Fetching from:
+
+ {activeFetchingScrapers.map((scraperName, index) => (
+
+ ))}
)}
-
-
- {sections.filter(Boolean).map((section, sectionIndex) => (
-
- {/* Section Header */}
- {renderSectionHeader({ section: section! })}
+
+ {/* Update the streams/loading state display logic */}
+ { showNoSourcesError ? (
+
+
+ No streaming sources available
+
+ Please add streaming sources in settings
+
+ navigation.navigate('Addons')}
+ >
+ Add Sources
+
+
+ ) : streamsEmpty ? (
+ showInitialLoading ? (
+
+
+
+ {isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
+
+
+ ) : showStillFetching ? (
+
+
+ Still fetching streams…
+
+ ) : (
+ // No streams and not loading = no streams available
+
+
+ No streams available
+
+ )
+ ) : (
+ // Show streams immediately when available, even if still loading others
+
+ {/* Show autoplay loading overlay if waiting for autoplay */}
+ {isAutoplayWaiting && !autoplayTriggered && (
+
+
+
+ Starting best stream...
+
+
+ )}
+
+
+ {sections.filter(Boolean).map((section, sectionIndex) => (
+
+ {/* Section Header */}
+ {renderSectionHeader({ section: section! })}
+
+ {/* Stream Cards using FlatList */}
+ {section!.data && section!.data.length > 0 ? (
+ {
+ if (item && item.url) {
+ return `${item.url}-${sectionIndex}-${index}`;
+ }
+ return `empty-${sectionIndex}-${index}`;
+ }}
+ renderItem={({ item, index }) => (
+
+ handleStreamPress(item)}
+ index={index}
+ isLoading={false}
+ statusMessage={undefined}
+ theme={currentTheme}
+ showLogos={settings.showScraperLogos}
+ scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogoCache.get((item.addonId || (item as any).addon) as string) || null : null}
+ showAlert={(t, m) => openAlert(t, m)}
+ parentTitle={metadata?.name}
+ parentType={type as 'movie' | 'series'}
+ parentSeason={(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined}
+ parentEpisode={(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined}
+ parentEpisodeTitle={(type === 'series' || type === 'other') ? currentEpisode?.name : undefined}
+ parentPosterUrl={episodeImage || metadata?.poster || undefined}
+ providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))}
+ parentId={id}
+ parentImdbId={imdbId || undefined}
+ />
+
+ )}
+ scrollEnabled={false}
+ initialNumToRender={6}
+ maxToRenderPerBatch={2}
+ windowSize={3}
+ removeClippedSubviews={true}
+ showsVerticalScrollIndicator={false}
+ getItemLayout={(data, index) => ({
+ length: 78, // Approximate height of StreamCard (68 minHeight + 10 marginBottom)
+ offset: 78 * index,
+ index,
+ })}
+ />
+ ) : null}
+
+ ))}
- {/* Stream Cards using FlatList */}
- {section!.data && section!.data.length > 0 ? (
- {
- if (item && item.url) {
- return `${item.url}-${sectionIndex}-${index}`;
- }
- return `empty-${sectionIndex}-${index}`;
- }}
- renderItem={({ item, index }) => (
-
- handleStreamPress(item)}
- index={index}
- isLoading={false}
- statusMessage={undefined}
- theme={currentTheme}
- showLogos={settings.showScraperLogos}
- scraperLogo={(item.addonId && scraperLogos[item.addonId]) || (item as any).addon ? scraperLogoCache.get((item.addonId || (item as any).addon) as string) || null : null}
- showAlert={(t, m) => openAlert(t, m)}
- parentTitle={metadata?.name}
- parentType={type as 'movie' | 'series'}
- parentSeason={(type === 'series' || type === 'other') ? currentEpisode?.season_number : undefined}
- parentEpisode={(type === 'series' || type === 'other') ? currentEpisode?.episode_number : undefined}
- parentEpisodeTitle={(type === 'series' || type === 'other') ? currentEpisode?.name : undefined}
- parentPosterUrl={episodeImage || metadata?.poster || undefined}
- providerName={streams && Object.keys(streams).find(pid => (streams as any)[pid]?.streams?.includes?.(item))}
- parentId={id}
- parentImdbId={imdbId || undefined}
- />
-
- )}
- scrollEnabled={false}
- initialNumToRender={6}
- maxToRenderPerBatch={2}
- windowSize={3}
- removeClippedSubviews={true}
- showsVerticalScrollIndicator={false}
- getItemLayout={(data, index) => ({
- length: 78, // Approximate height of StreamCard (68 minHeight + 10 marginBottom)
- offset: 78 * index,
- index,
- })}
- />
- ) : null}
-
- ))}
-
- {/* Footer Loading */}
- {(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
-
-
- Loading more sources...
-
- )}
-
+ {/* Footer Loading */}
+ {(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
+
+
+ Loading more sources...
+
+ )}
+
+
+ )}
- )}
-
+ >
+ )}
StyleSheet.create({
fontSize: 11,
fontWeight: '400',
},
+ // Tablet-specific styles
+ tabletLayout: {
+ flex: 1,
+ flexDirection: 'row',
+ },
+ tabletLeftPanel: {
+ width: '40%',
+ position: 'relative',
+ backgroundColor: colors.black,
+ },
+ tabletLeftPanelBackground: {
+ ...StyleSheet.absoluteFillObject,
+ },
+ tabletLeftPanelGradient: {
+ ...StyleSheet.absoluteFillObject,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 24,
+ },
+ tabletMovieLogoContainer: {
+ width: '80%',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ tabletMovieLogo: {
+ width: '100%',
+ height: 120,
+ marginBottom: 16,
+ },
+ tabletMovieTitle: {
+ color: colors.highEmphasis,
+ fontSize: 32,
+ fontWeight: '900',
+ textAlign: 'center',
+ letterSpacing: -0.5,
+ },
+ tabletEpisodeInfo: {
+ width: '80%',
+ },
+ tabletRightPanel: {
+ width: '60%',
+ flex: 1,
+ paddingTop: Platform.OS === 'android' ? 60 : 20,
+ },
+ backButtonContainerTablet: {
+ zIndex: 3,
+ },
});
export default memo(StreamsScreen);