From 635c97b1ad5bc5072095f377f0ccc1816ae60a6d Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 22 Oct 2025 23:46:50 +0530 Subject: [PATCH] more improvements --- src/screens/StreamsScreen.tsx | 627 +++++++++++++++++++++++----------- 1 file changed, 421 insertions(+), 206 deletions(-) diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index 22256bd7..12f366f0 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -51,6 +51,18 @@ import { useToast } from '../contexts/ToastContext'; import { useDownloads } from '../contexts/DownloadsContext'; import { streamCacheService } from '../services/streamCacheService'; import { PaperProvider } from 'react-native-paper'; +import { BlurView as ExpoBlurView } from 'expo-blur'; + +// Lazy-safe community blur import for Android +let AndroidBlurView: any = null; +if (Platform.OS === 'android') { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + AndroidBlurView = require('@react-native-community/blur').BlurView; + } catch (_) { + AndroidBlurView = null; + } +} const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906'; const HDR_ICON = 'https://uxwing.com/wp-content/themes/uxwing/download/video-photography-multimedia/hdr-icon.png'; @@ -2044,218 +2056,401 @@ export const StreamsScreen = () => { )} {isTablet ? ( - // TABLET LAYOUT + // TABLET LAYOUT - Full Screen Background - {/* Left Panel: Thumbnail Background */} + {/* Full Screen Background */} + + + + {/* Left Panel: Movie Logo/Episode Info */} - - - {type === 'movie' && metadata && ( - - {metadata.logo && !movieLogoError ? ( - setMovieLogoError(true)} - /> - ) : ( - {metadata.name} - )} - - )} - - {type === 'series' && currentEpisode && ( - - {currentEpisode.episodeString} - {currentEpisode.name} - {currentEpisode.overview && ( - {currentEpisode.overview} - )} - - )} - + {type === 'movie' && metadata && ( + + {metadata.logo && !movieLogoError ? ( + setMovieLogoError(true)} + /> + ) : ( + {metadata.name} + )} + + )} + + {type === 'series' && currentEpisode && ( + + {currentEpisode.episodeString} + {currentEpisode.name} + {currentEpisode.overview && ( + {currentEpisode.overview} + )} + + )} {/* Right Panel: Streams List */} - - - {!streamsEmpty && ( - - )} - + {Platform.OS === 'android' && AndroidBlurView ? ( + + + + {!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 && ( + {/* 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... + + )} + + + )} - )} - + + ) : ( + + + + {!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... + + )} + + + )} + + + )} ) : ( @@ -3043,20 +3238,20 @@ const createStyles = (colors: any) => StyleSheet.create({ tabletLayout: { flex: 1, flexDirection: 'row', + position: 'relative', + }, + tabletFullScreenBackground: { + ...StyleSheet.absoluteFillObject, + }, + tabletFullScreenGradient: { + ...StyleSheet.absoluteFillObject, }, tabletLeftPanel: { width: '40%', - position: 'relative', - backgroundColor: colors.black, - }, - tabletLeftPanelBackground: { - ...StyleSheet.absoluteFillObject, - }, - tabletLeftPanelGradient: { - ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', padding: 24, + zIndex: 2, }, tabletMovieLogoContainer: { width: '80%', @@ -3074,14 +3269,34 @@ const createStyles = (colors: any) => StyleSheet.create({ fontWeight: '900', textAlign: 'center', letterSpacing: -0.5, + textShadowColor: 'rgba(0,0,0,0.8)', + textShadowOffset: { width: 0, height: 2 }, + textShadowRadius: 4, }, tabletEpisodeInfo: { width: '80%', }, + tabletEpisodeText: { + textShadowColor: 'rgba(0,0,0,0.8)', + textShadowOffset: { width: 0, height: 2 }, + textShadowRadius: 4, + }, tabletRightPanel: { width: '60%', flex: 1, paddingTop: Platform.OS === 'android' ? 60 : 20, + zIndex: 2, + }, + tabletStreamsContent: { + backgroundColor: 'rgba(0,0,0,0.2)', + borderRadius: 24, + margin: 12, + overflow: 'hidden', // Ensures content respects rounded corners + }, + tabletBlurContent: { + flex: 1, + padding: 16, + backgroundColor: 'transparent', }, backButtonContainerTablet: { zIndex: 3,