more improvements
This commit is contained in:
parent
673c96c917
commit
635c97b1ad
1 changed files with 421 additions and 206 deletions
|
|
@ -51,6 +51,18 @@ import { useToast } from '../contexts/ToastContext';
|
||||||
import { useDownloads } from '../contexts/DownloadsContext';
|
import { useDownloads } from '../contexts/DownloadsContext';
|
||||||
import { streamCacheService } from '../services/streamCacheService';
|
import { streamCacheService } from '../services/streamCacheService';
|
||||||
import { PaperProvider } from 'react-native-paper';
|
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 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';
|
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 ? (
|
{isTablet ? (
|
||||||
// TABLET LAYOUT
|
// TABLET LAYOUT - Full Screen Background
|
||||||
<View style={styles.tabletLayout}>
|
<View style={styles.tabletLayout}>
|
||||||
{/* Left Panel: Thumbnail Background */}
|
{/* Full Screen Background */}
|
||||||
|
<AnimatedImage
|
||||||
|
source={episodeImage ? { uri: episodeImage } : bannerImage ? { uri: bannerImage } : metadata?.poster ? { uri: metadata.poster } : undefined}
|
||||||
|
style={styles.tabletFullScreenBackground}
|
||||||
|
contentFit="cover"
|
||||||
|
/>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(0,0,0,0.2)', 'rgba(0,0,0,0.4)', 'rgba(0,0,0,0.6)']}
|
||||||
|
locations={[0, 0.5, 1]}
|
||||||
|
style={styles.tabletFullScreenGradient}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Left Panel: Movie Logo/Episode Info */}
|
||||||
<View style={styles.tabletLeftPanel}>
|
<View style={styles.tabletLeftPanel}>
|
||||||
<AnimatedImage
|
{type === 'movie' && metadata && (
|
||||||
source={episodeImage ? { uri: episodeImage } : bannerImage ? { uri: bannerImage } : metadata?.poster ? { uri: metadata.poster } : undefined}
|
<View style={styles.tabletMovieLogoContainer}>
|
||||||
style={styles.tabletLeftPanelBackground}
|
{metadata.logo && !movieLogoError ? (
|
||||||
contentFit="cover"
|
<FastImage
|
||||||
/>
|
source={{ uri: metadata.logo }}
|
||||||
<LinearGradient
|
style={styles.tabletMovieLogo}
|
||||||
colors={['rgba(0,0,0,0.3)', 'rgba(0,0,0,0.5)']}
|
resizeMode={FastImage.resizeMode.contain}
|
||||||
style={styles.tabletLeftPanelGradient}
|
onError={() => setMovieLogoError(true)}
|
||||||
>
|
/>
|
||||||
{type === 'movie' && metadata && (
|
) : (
|
||||||
<View style={styles.tabletMovieLogoContainer}>
|
<Text style={styles.tabletMovieTitle}>{metadata.name}</Text>
|
||||||
{metadata.logo && !movieLogoError ? (
|
)}
|
||||||
<FastImage
|
</View>
|
||||||
source={{ uri: metadata.logo }}
|
)}
|
||||||
style={styles.tabletMovieLogo}
|
|
||||||
resizeMode={FastImage.resizeMode.contain}
|
{type === 'series' && currentEpisode && (
|
||||||
onError={() => setMovieLogoError(true)}
|
<View style={styles.tabletEpisodeInfo}>
|
||||||
/>
|
<Text style={[styles.streamsHeroEpisodeNumber, styles.tabletEpisodeText]}>{currentEpisode.episodeString}</Text>
|
||||||
) : (
|
<Text style={[styles.streamsHeroTitle, styles.tabletEpisodeText]} numberOfLines={2}>{currentEpisode.name}</Text>
|
||||||
<Text style={styles.tabletMovieTitle}>{metadata.name}</Text>
|
{currentEpisode.overview && (
|
||||||
)}
|
<Text style={[styles.streamsHeroOverview, styles.tabletEpisodeText]} numberOfLines={3}>{currentEpisode.overview}</Text>
|
||||||
</View>
|
)}
|
||||||
)}
|
</View>
|
||||||
|
)}
|
||||||
{type === 'series' && currentEpisode && (
|
|
||||||
<View style={styles.tabletEpisodeInfo}>
|
|
||||||
<Text style={styles.streamsHeroEpisodeNumber}>{currentEpisode.episodeString}</Text>
|
|
||||||
<Text style={styles.streamsHeroTitle} numberOfLines={2}>{currentEpisode.name}</Text>
|
|
||||||
{currentEpisode.overview && (
|
|
||||||
<Text style={styles.streamsHeroOverview} numberOfLines={3}>{currentEpisode.overview}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Right Panel: Streams List */}
|
{/* Right Panel: Streams List */}
|
||||||
<View style={styles.tabletRightPanel}>
|
<View style={styles.tabletRightPanel}>
|
||||||
<View style={[
|
{Platform.OS === 'android' && AndroidBlurView ? (
|
||||||
styles.streamsMainContent,
|
<AndroidBlurView
|
||||||
type === 'movie' && styles.streamsMainContentMovie
|
blurAmount={15}
|
||||||
]}>
|
blurRadius={8}
|
||||||
<View style={[styles.filterContainer]}>
|
style={[
|
||||||
{!streamsEmpty && (
|
styles.streamsMainContent,
|
||||||
<ProviderFilter
|
styles.tabletStreamsContent,
|
||||||
selectedProvider={selectedProvider}
|
type === 'movie' && styles.streamsMainContentMovie
|
||||||
providers={filterItems}
|
]}
|
||||||
onSelect={handleProviderChange}
|
>
|
||||||
theme={currentTheme}
|
<View style={styles.tabletBlurContent}>
|
||||||
/>
|
<View style={[styles.filterContainer]}>
|
||||||
)}
|
{!streamsEmpty && (
|
||||||
</View>
|
<ProviderFilter
|
||||||
|
selectedProvider={selectedProvider}
|
||||||
|
providers={filterItems}
|
||||||
|
onSelect={handleProviderChange}
|
||||||
|
theme={currentTheme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Active Scrapers Status */}
|
{/* Active Scrapers Status */}
|
||||||
{activeFetchingScrapers.length > 0 && (
|
{activeFetchingScrapers.length > 0 && (
|
||||||
<View
|
|
||||||
style={styles.activeScrapersContainer}
|
|
||||||
>
|
|
||||||
<Text style={styles.activeScrapersTitle}>Fetching from:</Text>
|
|
||||||
<View style={styles.activeScrapersRow}>
|
|
||||||
{activeFetchingScrapers.map((scraperName, index) => (
|
|
||||||
<PulsingChip key={scraperName} text={scraperName} delay={index * 200} />
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Update the streams/loading state display logic */}
|
|
||||||
{ showNoSourcesError ? (
|
|
||||||
<View
|
|
||||||
style={styles.noStreams}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
|
||||||
<Text style={styles.noStreamsText}>No streaming sources available</Text>
|
|
||||||
<Text style={styles.noStreamsSubText}>
|
|
||||||
Please add streaming sources in settings
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.addSourcesButton}
|
|
||||||
onPress={() => navigation.navigate('Addons')}
|
|
||||||
>
|
|
||||||
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
) : streamsEmpty ? (
|
|
||||||
showInitialLoading ? (
|
|
||||||
<View
|
|
||||||
style={styles.loadingContainer}
|
|
||||||
>
|
|
||||||
<ActivityIndicator size="large" color={colors.primary} />
|
|
||||||
<Text style={styles.loadingText}>
|
|
||||||
{isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : showStillFetching ? (
|
|
||||||
<View
|
|
||||||
style={styles.loadingContainer}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="hourglass-bottom" size={32} color={colors.primary} />
|
|
||||||
<Text style={styles.loadingText}>Still fetching streams…</Text>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
// No streams and not loading = no streams available
|
|
||||||
<View
|
|
||||||
style={styles.noStreams}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
|
||||||
<Text style={styles.noStreamsText}>No streams available</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
// Show streams immediately when available, even if still loading others
|
|
||||||
<View collapsable={false} style={{ flex: 1 }}>
|
|
||||||
{/* Show autoplay loading overlay if waiting for autoplay */}
|
|
||||||
{isAutoplayWaiting && !autoplayTriggered && (
|
|
||||||
<View
|
<View
|
||||||
style={styles.autoplayOverlay}
|
style={styles.activeScrapersContainer}
|
||||||
>
|
>
|
||||||
<View style={styles.autoplayIndicator}>
|
<Text style={styles.activeScrapersTitle}>Fetching from:</Text>
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
<View style={styles.activeScrapersRow}>
|
||||||
<Text style={styles.autoplayText}>Starting best stream...</Text>
|
{activeFetchingScrapers.map((scraperName, index) => (
|
||||||
|
<PulsingChip key={scraperName} text={scraperName} delay={index * 200} />
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ScrollView
|
{/* Update the streams/loading state display logic */}
|
||||||
style={styles.streamsContent}
|
{ showNoSourcesError ? (
|
||||||
contentContainerStyle={[
|
<View
|
||||||
styles.streamsContainer,
|
style={styles.noStreams}
|
||||||
{ paddingBottom: insets.bottom + 100 } // Add safe area + extra padding
|
>
|
||||||
]}
|
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
||||||
showsVerticalScrollIndicator={false}
|
<Text style={styles.noStreamsText}>No streaming sources available</Text>
|
||||||
bounces={true}
|
<Text style={styles.noStreamsSubText}>
|
||||||
overScrollMode="never"
|
Please add streaming sources in settings
|
||||||
// iOS-specific fixes for navigation transition glitches
|
</Text>
|
||||||
{...(Platform.OS === 'ios' && {
|
<TouchableOpacity
|
||||||
// Ensure proper rendering during transitions
|
style={styles.addSourcesButton}
|
||||||
removeClippedSubviews: false, // Prevent iOS from clipping views during transitions
|
onPress={() => navigation.navigate('Addons')}
|
||||||
// Force hardware acceleration for smoother transitions
|
>
|
||||||
scrollEventThrottle: 16,
|
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
|
||||||
})}
|
</TouchableOpacity>
|
||||||
>
|
</View>
|
||||||
{sections.filter(Boolean).map((section, sectionIndex) => (
|
) : streamsEmpty ? (
|
||||||
<View key={section!.addonId || sectionIndex}>
|
showInitialLoading ? (
|
||||||
{/* Section Header */}
|
<View
|
||||||
{renderSectionHeader({ section: section! })}
|
style={styles.loadingContainer}
|
||||||
|
>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
<Text style={styles.loadingText}>
|
||||||
|
{isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : showStillFetching ? (
|
||||||
|
<View
|
||||||
|
style={styles.loadingContainer}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="hourglass-bottom" size={32} color={colors.primary} />
|
||||||
|
<Text style={styles.loadingText}>Still fetching streams…</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
// No streams and not loading = no streams available
|
||||||
|
<View
|
||||||
|
style={styles.noStreams}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
||||||
|
<Text style={styles.noStreamsText}>No streams available</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
// Show streams immediately when available, even if still loading others
|
||||||
|
<View collapsable={false} style={{ flex: 1 }}>
|
||||||
|
{/* Show autoplay loading overlay if waiting for autoplay */}
|
||||||
|
{isAutoplayWaiting && !autoplayTriggered && (
|
||||||
|
<View
|
||||||
|
style={styles.autoplayOverlay}
|
||||||
|
>
|
||||||
|
<View style={styles.autoplayIndicator}>
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
<Text style={styles.autoplayText}>Starting best stream...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={styles.streamsContent}
|
||||||
|
contentContainerStyle={[
|
||||||
|
styles.streamsContainer,
|
||||||
|
{ paddingBottom: insets.bottom + 100 } // Add safe area + extra padding
|
||||||
|
]}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={true}
|
||||||
|
overScrollMode="never"
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
>
|
||||||
|
{sections.filter(Boolean).map((section, sectionIndex) => (
|
||||||
|
<View key={section!.addonId || sectionIndex}>
|
||||||
|
{/* Section Header */}
|
||||||
|
{renderSectionHeader({ section: section! })}
|
||||||
|
|
||||||
|
{/* Stream Cards using FlatList */}
|
||||||
|
{section!.data && section!.data.length > 0 ? (
|
||||||
|
<FlatList
|
||||||
|
data={section!.data}
|
||||||
|
keyExtractor={(item, index) => {
|
||||||
|
if (item && item.url) {
|
||||||
|
return `${item.url}-${sectionIndex}-${index}`;
|
||||||
|
}
|
||||||
|
return `empty-${sectionIndex}-${index}`;
|
||||||
|
}}
|
||||||
|
renderItem={({ item, index }) => (
|
||||||
|
<View>
|
||||||
|
<StreamCard
|
||||||
|
stream={item}
|
||||||
|
onPress={() => 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}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
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}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* Stream Cards using FlatList */}
|
{/* Footer Loading */}
|
||||||
{section!.data && section!.data.length > 0 ? (
|
{(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
|
||||||
<FlatList
|
<View style={styles.footerLoading}>
|
||||||
data={section!.data}
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
keyExtractor={(item, index) => {
|
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
||||||
if (item && item.url) {
|
</View>
|
||||||
return `${item.url}-${sectionIndex}-${index}`;
|
)}
|
||||||
}
|
</ScrollView>
|
||||||
return `empty-${sectionIndex}-${index}`;
|
</View>
|
||||||
}}
|
)}
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<View>
|
|
||||||
<StreamCard
|
|
||||||
stream={item}
|
|
||||||
onPress={() => 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}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
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}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{/* Footer Loading */}
|
|
||||||
{(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
|
|
||||||
<View style={styles.footerLoading}>
|
|
||||||
<ActivityIndicator size="small" color={colors.primary} />
|
|
||||||
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
</AndroidBlurView>
|
||||||
</View>
|
) : (
|
||||||
|
<ExpoBlurView
|
||||||
|
intensity={80}
|
||||||
|
tint="dark"
|
||||||
|
style={[
|
||||||
|
styles.streamsMainContent,
|
||||||
|
styles.tabletStreamsContent,
|
||||||
|
type === 'movie' && styles.streamsMainContentMovie
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.tabletBlurContent}>
|
||||||
|
<View style={[styles.filterContainer]}>
|
||||||
|
{!streamsEmpty && (
|
||||||
|
<ProviderFilter
|
||||||
|
selectedProvider={selectedProvider}
|
||||||
|
providers={filterItems}
|
||||||
|
onSelect={handleProviderChange}
|
||||||
|
theme={currentTheme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Active Scrapers Status */}
|
||||||
|
{activeFetchingScrapers.length > 0 && (
|
||||||
|
<View
|
||||||
|
style={styles.activeScrapersContainer}
|
||||||
|
>
|
||||||
|
<Text style={styles.activeScrapersTitle}>Fetching from:</Text>
|
||||||
|
<View style={styles.activeScrapersRow}>
|
||||||
|
{activeFetchingScrapers.map((scraperName, index) => (
|
||||||
|
<PulsingChip key={scraperName} text={scraperName} delay={index * 200} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Update the streams/loading state display logic */}
|
||||||
|
{ showNoSourcesError ? (
|
||||||
|
<View
|
||||||
|
style={styles.noStreams}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
||||||
|
<Text style={styles.noStreamsText}>No streaming sources available</Text>
|
||||||
|
<Text style={styles.noStreamsSubText}>
|
||||||
|
Please add streaming sources in settings
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.addSourcesButton}
|
||||||
|
onPress={() => navigation.navigate('Addons')}
|
||||||
|
>
|
||||||
|
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : streamsEmpty ? (
|
||||||
|
showInitialLoading ? (
|
||||||
|
<View
|
||||||
|
style={styles.loadingContainer}
|
||||||
|
>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
<Text style={styles.loadingText}>
|
||||||
|
{isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : showStillFetching ? (
|
||||||
|
<View
|
||||||
|
style={styles.loadingContainer}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="hourglass-bottom" size={32} color={colors.primary} />
|
||||||
|
<Text style={styles.loadingText}>Still fetching streams…</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
// No streams and not loading = no streams available
|
||||||
|
<View
|
||||||
|
style={styles.noStreams}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
||||||
|
<Text style={styles.noStreamsText}>No streams available</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
// Show streams immediately when available, even if still loading others
|
||||||
|
<View collapsable={false} style={{ flex: 1 }}>
|
||||||
|
{/* Show autoplay loading overlay if waiting for autoplay */}
|
||||||
|
{isAutoplayWaiting && !autoplayTriggered && (
|
||||||
|
<View
|
||||||
|
style={styles.autoplayOverlay}
|
||||||
|
>
|
||||||
|
<View style={styles.autoplayIndicator}>
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
<Text style={styles.autoplayText}>Starting best stream...</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={styles.streamsContent}
|
||||||
|
contentContainerStyle={[
|
||||||
|
styles.streamsContainer,
|
||||||
|
{ paddingBottom: insets.bottom + 100 } // Add safe area + extra padding
|
||||||
|
]}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={true}
|
||||||
|
overScrollMode="never"
|
||||||
|
// iOS-specific fixes for navigation transition glitches
|
||||||
|
{...(Platform.OS === 'ios' && {
|
||||||
|
// Ensure proper rendering during transitions
|
||||||
|
removeClippedSubviews: false, // Prevent iOS from clipping views during transitions
|
||||||
|
// Force hardware acceleration for smoother transitions
|
||||||
|
scrollEventThrottle: 16,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{sections.filter(Boolean).map((section, sectionIndex) => (
|
||||||
|
<View key={section!.addonId || sectionIndex}>
|
||||||
|
{/* Section Header */}
|
||||||
|
{renderSectionHeader({ section: section! })}
|
||||||
|
|
||||||
|
{/* Stream Cards using FlatList */}
|
||||||
|
{section!.data && section!.data.length > 0 ? (
|
||||||
|
<FlatList
|
||||||
|
data={section!.data}
|
||||||
|
keyExtractor={(item, index) => {
|
||||||
|
if (item && item.url) {
|
||||||
|
return `${item.url}-${sectionIndex}-${index}`;
|
||||||
|
}
|
||||||
|
return `empty-${sectionIndex}-${index}`;
|
||||||
|
}}
|
||||||
|
renderItem={({ item, index }) => (
|
||||||
|
<View>
|
||||||
|
<StreamCard
|
||||||
|
stream={item}
|
||||||
|
onPress={() => 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}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
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}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Footer Loading */}
|
||||||
|
{(loadingStreams || loadingEpisodeStreams) && hasStremioStreamProviders && (
|
||||||
|
<View style={styles.footerLoading}>
|
||||||
|
<ActivityIndicator size="small" color={colors.primary} />
|
||||||
|
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ExpoBlurView>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -3043,20 +3238,20 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
tabletLayout: {
|
tabletLayout: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
tabletFullScreenBackground: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
},
|
||||||
|
tabletFullScreenGradient: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
},
|
},
|
||||||
tabletLeftPanel: {
|
tabletLeftPanel: {
|
||||||
width: '40%',
|
width: '40%',
|
||||||
position: 'relative',
|
|
||||||
backgroundColor: colors.black,
|
|
||||||
},
|
|
||||||
tabletLeftPanelBackground: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
},
|
|
||||||
tabletLeftPanelGradient: {
|
|
||||||
...StyleSheet.absoluteFillObject,
|
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 24,
|
padding: 24,
|
||||||
|
zIndex: 2,
|
||||||
},
|
},
|
||||||
tabletMovieLogoContainer: {
|
tabletMovieLogoContainer: {
|
||||||
width: '80%',
|
width: '80%',
|
||||||
|
|
@ -3074,14 +3269,34 @@ const createStyles = (colors: any) => StyleSheet.create({
|
||||||
fontWeight: '900',
|
fontWeight: '900',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
letterSpacing: -0.5,
|
letterSpacing: -0.5,
|
||||||
|
textShadowColor: 'rgba(0,0,0,0.8)',
|
||||||
|
textShadowOffset: { width: 0, height: 2 },
|
||||||
|
textShadowRadius: 4,
|
||||||
},
|
},
|
||||||
tabletEpisodeInfo: {
|
tabletEpisodeInfo: {
|
||||||
width: '80%',
|
width: '80%',
|
||||||
},
|
},
|
||||||
|
tabletEpisodeText: {
|
||||||
|
textShadowColor: 'rgba(0,0,0,0.8)',
|
||||||
|
textShadowOffset: { width: 0, height: 2 },
|
||||||
|
textShadowRadius: 4,
|
||||||
|
},
|
||||||
tabletRightPanel: {
|
tabletRightPanel: {
|
||||||
width: '60%',
|
width: '60%',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingTop: Platform.OS === 'android' ? 60 : 20,
|
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: {
|
backButtonContainerTablet: {
|
||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue