Revert "Refactor StreamsScreen to streamline component structure and enhance readability; replace inline components with imports for MovieHero and EpisodeHero, and utilize custom hooks for provider management. Optimize loading logic and animation effects, while removing unused code and improving overall performance."

This reverts commit 3b6fb438e3.
This commit is contained in:
tapframe 2025-05-03 14:30:27 +05:30
parent 3b6fb438e3
commit dfda3ff38a
7 changed files with 906 additions and 1034 deletions

View file

@ -1,223 +0,0 @@
import React from 'react';
import { StyleSheet, View, Text, ImageBackground } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, { FadeIn } from 'react-native-reanimated';
import { colors } from '../../styles/colors';
import { tmdbService } from '../../services/tmdbService';
const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906';
interface EpisodeHeroProps {
currentEpisode: {
name: string;
overview?: string;
still_path?: string;
air_date?: string | null;
vote_average?: number;
runtime?: number;
episodeString: string;
season_number?: number;
episode_number?: number;
} | null;
metadata: {
poster?: string;
} | null;
animatedStyle: any;
}
const EpisodeHero = ({ currentEpisode, metadata, animatedStyle }: EpisodeHeroProps) => {
if (!currentEpisode) return null;
const episodeImage = currentEpisode.still_path
? tmdbService.getImageUrl(currentEpisode.still_path, 'original')
: metadata?.poster || null;
// Format air date safely
const formattedAirDate = currentEpisode.air_date !== undefined
? tmdbService.formatAirDate(currentEpisode.air_date)
: 'Unknown';
return (
<Animated.View style={[styles.streamsHeroContainer, animatedStyle]}>
<Animated.View
entering={FadeIn.duration(600).springify()}
style={StyleSheet.absoluteFill}
>
<Animated.View
entering={FadeIn.duration(800).delay(100).springify().withInitialValues({
transform: [{ scale: 1.05 }]
})}
style={StyleSheet.absoluteFill}
>
<ImageBackground
source={episodeImage ? { uri: episodeImage } : undefined}
style={styles.streamsHeroBackground}
fadeDuration={0}
resizeMode="cover"
>
<LinearGradient
colors={[
'rgba(0,0,0,0)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.7)',
'rgba(0,0,0,0.85)',
'rgba(0,0,0,0.95)',
colors.darkBackground
]}
locations={[0, 0.3, 0.5, 0.7, 0.85, 1]}
style={styles.streamsHeroGradient}
>
<View style={styles.streamsHeroContent}>
<View style={styles.streamsHeroInfo}>
<Text style={styles.streamsHeroEpisodeNumber}>
{currentEpisode.episodeString}
</Text>
<Text style={styles.streamsHeroTitle} numberOfLines={1}>
{currentEpisode.name}
</Text>
{currentEpisode.overview && (
<Text style={styles.streamsHeroOverview} numberOfLines={2}>
{currentEpisode.overview}
</Text>
)}
<View style={styles.streamsHeroMeta}>
<Text style={styles.streamsHeroReleased}>
{formattedAirDate}
</Text>
{currentEpisode.vote_average && currentEpisode.vote_average > 0 && (
<View style={styles.streamsHeroRating}>
<Image
source={{ uri: TMDB_LOGO }}
style={styles.tmdbLogo}
contentFit="contain"
/>
<Text style={styles.streamsHeroRatingText}>
{currentEpisode.vote_average.toFixed(1)}
</Text>
</View>
)}
{currentEpisode.runtime && (
<View style={styles.streamsHeroRuntime}>
<MaterialIcons name="schedule" size={16} color={colors.mediumEmphasis} />
<Text style={styles.streamsHeroRuntimeText}>
{currentEpisode.runtime >= 60
? `${Math.floor(currentEpisode.runtime / 60)}h ${currentEpisode.runtime % 60}m`
: `${currentEpisode.runtime}m`}
</Text>
</View>
)}
</View>
</View>
</View>
</LinearGradient>
</ImageBackground>
</Animated.View>
</Animated.View>
</Animated.View>
);
};
const styles = StyleSheet.create({
streamsHeroContainer: {
width: '100%',
height: 300,
marginBottom: 0,
position: 'relative',
backgroundColor: colors.black,
pointerEvents: 'box-none',
},
streamsHeroBackground: {
width: '100%',
height: '100%',
backgroundColor: colors.black,
},
streamsHeroGradient: {
flex: 1,
justifyContent: 'flex-end',
padding: 16,
paddingBottom: 0,
},
streamsHeroContent: {
width: '100%',
},
streamsHeroInfo: {
width: '100%',
},
streamsHeroEpisodeNumber: {
color: colors.primary,
fontSize: 14,
fontWeight: 'bold',
marginBottom: 2,
textShadowColor: 'rgba(0,0,0,0.75)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
},
streamsHeroTitle: {
color: colors.highEmphasis,
fontSize: 24,
fontWeight: 'bold',
marginBottom: 4,
textShadowColor: 'rgba(0,0,0,0.75)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
},
streamsHeroOverview: {
color: colors.mediumEmphasis,
fontSize: 14,
lineHeight: 20,
marginBottom: 2,
textShadowColor: 'rgba(0,0,0,0.75)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
},
streamsHeroMeta: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
marginTop: 0,
},
streamsHeroReleased: {
color: colors.mediumEmphasis,
fontSize: 14,
textShadowColor: 'rgba(0,0,0,0.75)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
},
streamsHeroRating: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.7)',
paddingHorizontal: 6,
paddingVertical: 3,
borderRadius: 4,
marginTop: 0,
},
tmdbLogo: {
width: 20,
height: 14,
},
streamsHeroRatingText: {
color: '#01b4e4',
fontSize: 13,
fontWeight: '700',
marginLeft: 4,
},
streamsHeroRuntime: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
backgroundColor: 'rgba(0,0,0,0.5)',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
},
streamsHeroRuntimeText: {
color: colors.mediumEmphasis,
fontSize: 13,
fontWeight: '600',
},
});
export default React.memo(EpisodeHero);

View file

@ -1,98 +0,0 @@
import React from 'react';
import { StyleSheet, Text, View, ImageBackground, Dimensions, Platform } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Image } from 'expo-image';
import Animated from 'react-native-reanimated';
import { colors } from '../../styles/colors';
const { width } = Dimensions.get('window');
interface MovieHeroProps {
metadata: {
name: string;
logo?: string;
banner?: string;
poster?: string;
} | null;
animatedStyle: any;
}
const MovieHero = ({ metadata, animatedStyle }: MovieHeroProps) => {
if (!metadata) return null;
return (
<Animated.View style={[styles.movieTitleContainer, animatedStyle]}>
<ImageBackground
source={{ uri: metadata.banner || metadata.poster }}
style={styles.movieTitleBackground}
resizeMode="cover"
>
<LinearGradient
colors={[
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.6)',
'rgba(0,0,0,0.8)',
colors.darkBackground
]}
locations={[0, 0.3, 0.7, 1]}
style={styles.movieTitleGradient}
>
<View style={styles.movieTitleContent}>
{metadata.logo ? (
<Image
source={{ uri: metadata.logo }}
style={styles.movieLogo}
contentFit="contain"
/>
) : (
<Text style={styles.movieTitle} numberOfLines={2}>
{metadata.name}
</Text>
)}
</View>
</LinearGradient>
</ImageBackground>
</Animated.View>
);
};
const styles = StyleSheet.create({
movieTitleContainer: {
width: '100%',
height: 180,
backgroundColor: colors.black,
pointerEvents: 'box-none',
},
movieTitleBackground: {
width: '100%',
height: '100%',
backgroundColor: colors.black,
},
movieTitleGradient: {
flex: 1,
justifyContent: 'center',
padding: 16,
},
movieTitleContent: {
width: '100%',
alignItems: 'center',
marginTop: Platform.OS === 'android' ? 35 : 45,
},
movieLogo: {
width: width * 0.6,
height: 70,
marginBottom: 8,
},
movieTitle: {
color: colors.highEmphasis,
fontSize: 28,
fontWeight: '900',
textAlign: 'center',
textShadowColor: 'rgba(0,0,0,0.75)',
textShadowOffset: { width: 0, height: 2 },
textShadowRadius: 4,
letterSpacing: -0.5,
},
});
export default React.memo(MovieHero);

View file

@ -1,80 +0,0 @@
import React, { useCallback } from 'react';
import { FlatList, StyleSheet, Text, TouchableOpacity } from 'react-native';
import { colors } from '../../styles/colors';
interface ProviderFilterProps {
selectedProvider: string;
providers: Array<{ id: string; name: string; }>;
onSelect: (id: string) => void;
}
const ProviderFilter = ({ selectedProvider, providers, onSelect }: ProviderFilterProps) => {
const renderItem = useCallback(({ item }: { item: { id: string; name: string } }) => (
<TouchableOpacity
key={item.id}
style={[
styles.filterChip,
selectedProvider === item.id && styles.filterChipSelected
]}
onPress={() => onSelect(item.id)}
>
<Text style={[
styles.filterChipText,
selectedProvider === item.id && styles.filterChipTextSelected
]}>
{item.name}
</Text>
</TouchableOpacity>
), [selectedProvider, onSelect]);
return (
<FlatList
data={providers}
renderItem={renderItem}
keyExtractor={item => item.id}
horizontal
showsHorizontalScrollIndicator={false}
style={styles.filterScroll}
bounces={true}
overScrollMode="never"
decelerationRate="fast"
initialNumToRender={5}
maxToRenderPerBatch={3}
windowSize={3}
getItemLayout={(data, index) => ({
length: 100, // Approximate width of each item
offset: 100 * index,
index,
})}
/>
);
};
const styles = StyleSheet.create({
filterScroll: {
flexGrow: 0,
},
filterChip: {
backgroundColor: colors.transparentLight,
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
marginRight: 8,
borderWidth: 1,
borderColor: colors.transparent,
},
filterChipSelected: {
backgroundColor: colors.transparentLight,
borderColor: colors.primary,
},
filterChipText: {
color: colors.text,
fontWeight: '500',
},
filterChipTextSelected: {
color: colors.primary,
fontWeight: 'bold',
},
});
export default React.memo(ProviderFilter);

View file

@ -1,181 +0,0 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../../styles/colors';
import { Stream } from '../../types/metadata';
import QualityBadge from '../metadata/QualityBadge';
interface StreamCardProps {
stream: Stream;
onPress: () => void;
index: number;
isLoading?: boolean;
statusMessage?: string;
}
const StreamCard = ({ stream, onPress, index, isLoading, statusMessage }: StreamCardProps) => {
const quality = stream.title?.match(/(\d+)p/)?.[1] || null;
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 displayTitle = stream.name || stream.title || 'Unnamed Stream';
const displayAddonName = stream.title || '';
return (
<TouchableOpacity
style={[
styles.streamCard,
isLoading && styles.streamCardLoading
]}
onPress={onPress}
disabled={isLoading}
activeOpacity={0.7}
>
<View style={styles.streamDetails}>
<View style={styles.streamNameRow}>
<View style={styles.streamTitleContainer}>
<Text style={styles.streamName}>
{displayTitle}
</Text>
{displayAddonName && displayAddonName !== displayTitle && (
<Text style={styles.streamAddonName}>
{displayAddonName}
</Text>
)}
</View>
{/* Show loading indicator if stream is loading */}
{isLoading && (
<View style={styles.loadingIndicator}>
<ActivityIndicator size="small" color={colors.primary} />
<Text style={styles.loadingText}>
{statusMessage || "Loading..."}
</Text>
</View>
)}
</View>
<View style={styles.streamMetaRow}>
{quality && quality >= "720" && (
<QualityBadge type="HD" />
)}
{isDolby && (
<QualityBadge type="VISION" />
)}
{size && (
<View style={[styles.chip, { backgroundColor: colors.darkGray }]}>
<Text style={styles.chipText}>{size}</Text>
</View>
)}
{isDebrid && (
<View style={[styles.chip, { backgroundColor: colors.success }]}>
<Text style={styles.chipText}>DEBRID</Text>
</View>
)}
</View>
</View>
<View style={styles.streamAction}>
<MaterialIcons
name="play-arrow"
size={24}
color={colors.primary}
/>
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
streamCard: {
flexDirection: 'row',
alignItems: 'flex-start',
padding: 12,
borderRadius: 12,
marginBottom: 8,
minHeight: 70,
backgroundColor: colors.elevation1,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.05)',
width: '100%',
zIndex: 1,
},
streamCardLoading: {
opacity: 0.7,
},
streamDetails: {
flex: 1,
},
streamNameRow: {
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
width: '100%',
flexWrap: 'wrap',
gap: 8
},
streamTitleContainer: {
flex: 1,
},
streamName: {
fontSize: 14,
fontWeight: '600',
marginBottom: 2,
lineHeight: 20,
color: colors.highEmphasis,
},
streamAddonName: {
fontSize: 13,
lineHeight: 18,
color: colors.mediumEmphasis,
marginBottom: 6,
},
streamMetaRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 4,
marginBottom: 6,
alignItems: 'center',
},
chip: {
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 4,
marginRight: 4,
marginBottom: 4,
},
chipText: {
color: colors.highEmphasis,
fontSize: 12,
fontWeight: '600',
},
loadingIndicator: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 12,
marginLeft: 8,
},
loadingText: {
color: colors.primary,
fontSize: 12,
marginLeft: 4,
fontWeight: '500',
},
streamAction: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.elevation2,
justifyContent: 'center',
alignItems: 'center',
},
});
export default React.memo(StreamCard);

View file

@ -1,221 +0,0 @@
import { useCallback } from 'react';
import { Platform, Linking } from 'react-native';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import { Stream } from '../types/metadata';
import { logger } from '../utils/logger';
interface UseStreamNavigationProps {
metadata: {
name?: string;
year?: number;
} | null;
currentEpisode?: {
name?: string;
season_number?: number;
episode_number?: number;
} | null;
id: string;
type: string;
selectedEpisode?: string;
useExternalPlayer?: boolean;
preferredPlayer?: 'internal' | 'vlc' | 'outplayer' | 'infuse' | 'vidhub' | 'external';
}
export const useStreamNavigation = ({
metadata,
currentEpisode,
id,
type,
selectedEpisode,
useExternalPlayer,
preferredPlayer
}: UseStreamNavigationProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const navigateToPlayer = useCallback((stream: Stream) => {
navigation.navigate('Player', {
uri: stream.url,
title: metadata?.name || '',
episodeTitle: type === 'series' ? currentEpisode?.name : undefined,
season: type === 'series' ? currentEpisode?.season_number : undefined,
episode: type === 'series' ? currentEpisode?.episode_number : undefined,
quality: stream.title?.match(/(\d+)p/)?.[1] || undefined,
year: metadata?.year,
streamProvider: stream.name,
id,
type,
episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined
});
}, [metadata, type, currentEpisode, navigation, id, selectedEpisode]);
const handleStreamPress = useCallback(async (stream: Stream) => {
try {
if (stream.url) {
logger.log('handleStreamPress called with stream:', {
url: stream.url,
behaviorHints: stream.behaviorHints,
useExternalPlayer,
preferredPlayer
});
// For iOS, try to open with the preferred external player
if (Platform.OS === 'ios' && preferredPlayer !== 'internal') {
try {
// Format the URL for the selected player
const streamUrl = encodeURIComponent(stream.url);
let externalPlayerUrls: string[] = [];
// Configure URL formats based on the selected player
switch (preferredPlayer) {
case 'vlc':
externalPlayerUrls = [
`vlc://${stream.url}`,
`vlc-x-callback://x-callback-url/stream?url=${streamUrl}`,
`vlc://${streamUrl}`
];
break;
case 'outplayer':
externalPlayerUrls = [
`outplayer://${stream.url}`,
`outplayer://${streamUrl}`,
`outplayer://play?url=${streamUrl}`,
`outplayer://stream?url=${streamUrl}`,
`outplayer://play/browser?url=${streamUrl}`
];
break;
case 'infuse':
externalPlayerUrls = [
`infuse://x-callback-url/play?url=${streamUrl}`,
`infuse://play?url=${streamUrl}`,
`infuse://${streamUrl}`
];
break;
case 'vidhub':
externalPlayerUrls = [
`vidhub://play?url=${streamUrl}`,
`vidhub://${streamUrl}`
];
break;
default:
// If no matching player or the setting is somehow invalid, use internal player
navigateToPlayer(stream);
return;
}
console.log(`Attempting to open stream in ${preferredPlayer}`);
// Try each URL format in sequence
const tryNextUrl = (index: number) => {
if (index >= externalPlayerUrls.length) {
console.log(`All ${preferredPlayer} formats failed, falling back to direct URL`);
// Try direct URL as last resort
Linking.openURL(stream.url)
.then(() => console.log('Opened with direct URL'))
.catch(() => {
console.log('Direct URL failed, falling back to built-in player');
navigateToPlayer(stream);
});
return;
}
const url = externalPlayerUrls[index];
console.log(`Trying ${preferredPlayer} URL format ${index + 1}: ${url}`);
Linking.openURL(url)
.then(() => console.log(`Successfully opened stream with ${preferredPlayer} format ${index + 1}`))
.catch(err => {
console.log(`Format ${index + 1} failed: ${err.message}`, err);
tryNextUrl(index + 1);
});
};
// Start with the first URL format
tryNextUrl(0);
} catch (error) {
console.error(`Error with ${preferredPlayer}:`, error);
// Fallback to the built-in player
navigateToPlayer(stream);
}
}
// For Android with external player preference
else if (Platform.OS === 'android' && useExternalPlayer) {
try {
console.log('Opening stream with Android native app chooser');
// For Android, determine if the URL is a direct http/https URL or a magnet link
const isMagnet = stream.url.startsWith('magnet:');
if (isMagnet) {
// For magnet links, open directly which will trigger the torrent app chooser
console.log('Opening magnet link directly');
Linking.openURL(stream.url)
.then(() => console.log('Successfully opened magnet link'))
.catch(err => {
console.error('Failed to open magnet link:', err);
// No good fallback for magnet links
navigateToPlayer(stream);
});
} else {
// For direct video URLs, use the S.Browser.ACTION_VIEW approach
// This is a more reliable way to force Android to show all video apps
// Strip query parameters if they exist as they can cause issues with some apps
let cleanUrl = stream.url;
if (cleanUrl.includes('?')) {
cleanUrl = cleanUrl.split('?')[0];
}
// Create an Android intent URL that forces the chooser
// Set component=null to ensure chooser is shown
// Set action=android.intent.action.VIEW to open the content
const intentUrl = `intent:${cleanUrl}#Intent;action=android.intent.action.VIEW;category=android.intent.category.DEFAULT;component=;type=video/*;launchFlags=0x10000000;end`;
console.log(`Using intent URL: ${intentUrl}`);
Linking.openURL(intentUrl)
.then(() => console.log('Successfully opened with intent URL'))
.catch(err => {
console.error('Failed to open with intent URL:', err);
// First fallback: Try direct URL with regular Linking API
console.log('Trying plain URL as fallback');
Linking.openURL(stream.url)
.then(() => console.log('Opened with direct URL'))
.catch(directErr => {
console.error('Failed to open direct URL:', directErr);
// Final fallback: Use built-in player
console.log('All external player attempts failed, using built-in player');
navigateToPlayer(stream);
});
});
}
} catch (error) {
console.error('Error with external player:', error);
// Fallback to the built-in player
navigateToPlayer(stream);
}
}
else {
// For internal player or if other options failed, use the built-in player
navigateToPlayer(stream);
}
}
} catch (error) {
console.error('Error in handleStreamPress:', error);
// Final fallback: Use built-in player
navigateToPlayer(stream);
}
}, [navigateToPlayer, preferredPlayer, useExternalPlayer]);
return {
handleStreamPress,
navigateToPlayer
};
};

View file

@ -1,146 +0,0 @@
import { useState, useEffect, useMemo, useCallback } from 'react';
import { stremioService } from '../services/stremioService';
import { Stream } from '../types/metadata';
import { logger } from '../utils/logger';
interface StreamGroups {
[addonId: string]: {
addonName: string;
streams: Stream[];
};
}
export const useStreamProviders = (
groupedStreams: StreamGroups,
episodeStreams: StreamGroups,
type: string,
loadingStreams: boolean,
loadingEpisodeStreams: boolean
) => {
const [selectedProvider, setSelectedProvider] = useState('all');
const [availableProviders, setAvailableProviders] = useState<Set<string>>(new Set());
const [loadingProviders, setLoadingProviders] = useState<{[key: string]: boolean}>({});
const [providerStatus, setProviderStatus] = useState<{
[key: string]: {
loading: boolean;
success: boolean;
error: boolean;
message: string;
timeStarted: number;
timeCompleted: number;
}
}>({});
const [providerLoadTimes, setProviderLoadTimes] = useState<{[key: string]: number}>({});
const [loadStartTime, setLoadStartTime] = useState(0);
// Update available providers when streams change - converted to useEffect
useEffect(() => {
const streams = type === 'series' ? episodeStreams : groupedStreams;
const providers = new Set(Object.keys(streams));
setAvailableProviders(providers);
}, [type, groupedStreams, episodeStreams]);
// Start tracking load time when loading begins - converted to useEffect
useEffect(() => {
if (loadingStreams || loadingEpisodeStreams) {
logger.log("⏱️ Stream loading started");
const now = Date.now();
setLoadStartTime(now);
setProviderLoadTimes({});
// Reset provider status - only for stremio addons
setProviderStatus({
'stremio': {
loading: true,
success: false,
error: false,
message: 'Loading...',
timeStarted: now,
timeCompleted: 0
}
});
// Also update the simpler loading state - only for stremio
setLoadingProviders({
'stremio': true
});
}
}, [loadingStreams, loadingEpisodeStreams]);
// Generate filter items for the provider selector
const filterItems = useMemo(() => {
const installedAddons = stremioService.getInstalledAddons();
const streams = type === 'series' ? episodeStreams : groupedStreams;
return [
{ id: 'all', name: 'All Providers' },
...Array.from(availableProviders)
.sort((a, b) => {
const indexA = installedAddons.findIndex(addon => addon.id === a);
const indexB = installedAddons.findIndex(addon => addon.id === b);
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
if (indexA !== -1) return -1;
if (indexB !== -1) return 1;
return 0;
})
.map(provider => {
const addonInfo = streams[provider];
const installedAddon = installedAddons.find(addon => addon.id === provider);
let displayName = provider;
if (installedAddon) displayName = installedAddon.name;
else if (addonInfo?.addonName) displayName = addonInfo.addonName;
return { id: provider, name: displayName };
})
];
}, [availableProviders, type, episodeStreams, groupedStreams]);
// Filter streams to show only selected provider (or all)
const filteredSections = useMemo(() => {
const streams = type === 'series' ? episodeStreams : groupedStreams;
const installedAddons = stremioService.getInstalledAddons();
return Object.entries(streams)
.filter(([addonId]) => {
// If "all" is selected, show all providers
if (selectedProvider === 'all') {
return true;
}
// Otherwise only show the selected provider
return addonId === selectedProvider;
})
.sort(([addonIdA], [addonIdB]) => {
const indexA = installedAddons.findIndex(addon => addon.id === addonIdA);
const indexB = installedAddons.findIndex(addon => addon.id === addonIdB);
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
if (indexA !== -1) return -1;
if (indexB !== -1) return 1;
return 0;
})
.map(([addonId, { addonName, streams }]) => ({
title: addonName,
addonId,
data: streams
}));
}, [selectedProvider, type, episodeStreams, groupedStreams]);
// Handler for changing the selected provider
const handleProviderChange = useCallback((provider: string) => {
setSelectedProvider(provider);
}, []);
return {
selectedProvider,
availableProviders,
loadingProviders,
providerStatus,
filterItems,
filteredSections,
handleProviderChange,
setLoadingProviders,
setProviderStatus
};
};

File diff suppressed because it is too large Load diff