This commit is contained in:
tapframe 2025-07-29 01:13:34 +05:30
parent 494b35b1c0
commit ebcbced142
3 changed files with 142 additions and 35 deletions

View file

@ -41,6 +41,7 @@ export interface AppSettings {
enableLocalScrapers: boolean; // Enable/disable local scraper functionality enableLocalScrapers: boolean; // Enable/disable local scraper functionality
scraperTimeout: number; // Timeout for scraper execution in seconds scraperTimeout: number; // Timeout for scraper execution in seconds
enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers enableScraperUrlValidation: boolean; // Enable/disable URL validation for scrapers
streamDisplayMode: 'separate' | 'grouped'; // How to display streaming links - separately by provider or grouped under one name
} }
export const DEFAULT_SETTINGS: AppSettings = { export const DEFAULT_SETTINGS: AppSettings = {
@ -64,6 +65,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
enableLocalScrapers: true, enableLocalScrapers: true,
scraperTimeout: 60, // 60 seconds timeout scraperTimeout: 60, // 60 seconds timeout
enableScraperUrlValidation: true, // Enable URL validation by default enableScraperUrlValidation: true, // Enable URL validation by default
streamDisplayMode: 'separate', // Default to separate display by provider
}; };
const SETTINGS_STORAGE_KEY = 'app_settings'; const SETTINGS_STORAGE_KEY = 'app_settings';

View file

@ -739,6 +739,22 @@ const PluginsScreen: React.FC = () => {
disabled={!settings.enableLocalScrapers} disabled={!settings.enableLocalScrapers}
/> />
</View> </View>
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={[styles.settingTitle, !settings.enableLocalScrapers && styles.disabledText]}>Stream Display Mode</Text>
<Text style={[styles.settingDescription, !settings.enableLocalScrapers && styles.disabledText]}>
{settings.streamDisplayMode === 'separate' ? 'Show each provider separately' : 'Group all providers under one name'}
</Text>
</View>
<Switch
value={settings.streamDisplayMode === 'grouped'}
onValueChange={(value) => updateSetting('streamDisplayMode', value ? 'grouped' : 'separate')}
trackColor={{ false: colors.elevation3, true: colors.primary }}
thumbColor={settings.streamDisplayMode === 'grouped' ? colors.white : '#f4f3f4'}
disabled={!settings.enableLocalScrapers}
/>
</View>
</View> </View>

View file

@ -101,15 +101,8 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
}; };
}, [stream.name, stream.title, stream.behaviorHints, stream.size]); }, [stream.name, stream.title, stream.behaviorHints, stream.size]);
// Animation delay based on index - stagger effect
const enterDelay = 100 + (index * 30);
return ( return (
<Animated.View <TouchableOpacity
entering={FadeInDown.duration(200).delay(enterDelay)}
exiting={FadeOut.duration(150)}
>
<TouchableOpacity
style={[ style={[
styles.streamCard, styles.streamCard,
isLoading && styles.streamCardLoading isLoading && styles.streamCardLoading
@ -169,7 +162,6 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
/> />
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</Animated.View>
); );
}); });
@ -232,11 +224,7 @@ const ProviderFilter = memo(({
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]); const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => ( const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
<Animated.View <TouchableOpacity
entering={FadeIn.duration(300).delay(100 + index * 40)}
exiting={FadeOut.duration(150)}
>
<TouchableOpacity
key={item.id} key={item.id}
style={[ style={[
styles.filterChip, styles.filterChip,
@ -251,13 +239,10 @@ const ProviderFilter = memo(({
{item.name} {item.name}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</Animated.View>
), [selectedProvider, onSelect, styles]); ), [selectedProvider, onSelect, styles]);
return ( return (
<Animated.View <View>
entering={FadeIn.duration(300)}
>
<FlatList <FlatList
data={providers} data={providers}
renderItem={renderItem} renderItem={renderItem}
@ -277,7 +262,7 @@ const ProviderFilter = memo(({
index, index,
})} })}
/> />
</Animated.View> </View>
); );
}); });
@ -385,7 +370,12 @@ export const StreamsScreen = () => {
if (providersWithStreams.length > 0) { if (providersWithStreams.length > 0) {
logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`); logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`);
const providersWithStreamsSet = new Set(providersWithStreams); const providersWithStreamsSet = new Set(providersWithStreams);
setAvailableProviders(providersWithStreamsSet);
// Only update if we have new providers, don't remove existing ones during loading
setAvailableProviders(prevProviders => {
const newProviders = new Set([...prevProviders, ...providersWithStreamsSet]);
return newProviders;
});
} }
// Update loading states for individual providers // Update loading states for individual providers
@ -407,11 +397,28 @@ export const StreamsScreen = () => {
}, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type]); }, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type]);
// Reset the selected provider to 'all' if the current selection is no longer available // Reset the selected provider to 'all' if the current selection is no longer available
// But preserve special filter values like 'grouped-plugins' and 'all'
useEffect(() => { useEffect(() => {
if (selectedProvider !== 'all' && !availableProviders.has(selectedProvider)) { // Don't reset if it's a special filter value
const isSpecialFilter = selectedProvider === 'all' || selectedProvider === 'grouped-plugins';
if (isSpecialFilter) {
return; // Always preserve special filters
}
// Check if provider exists in current streams data
const currentStreamsData = type === 'series' ? episodeStreams : groupedStreams;
const hasStreamsForProvider = currentStreamsData[selectedProvider] &&
currentStreamsData[selectedProvider].streams &&
currentStreamsData[selectedProvider].streams.length > 0;
// Only reset if the provider doesn't exist in available providers AND doesn't have streams
const isAvailableProvider = availableProviders.has(selectedProvider);
if (!isAvailableProvider && !hasStreamsForProvider) {
setSelectedProvider('all'); setSelectedProvider('all');
} }
}, [selectedProvider, availableProviders]); }, [selectedProvider, availableProviders, episodeStreams, groupedStreams, type]);
// Update useEffect to check for sources // Update useEffect to check for sources
useEffect(() => { useEffect(() => {
@ -863,6 +870,43 @@ export const StreamsScreen = () => {
) )
]); ]);
// In grouped mode, separate addons and plugins
if (settings.streamDisplayMode === 'grouped') {
const addonProviders: string[] = [];
const pluginProviders: string[] = [];
Array.from(allProviders).forEach(provider => {
const isInstalledAddon = installedAddons.some(addon => addon.id === provider);
if (isInstalledAddon) {
addonProviders.push(provider);
} else {
pluginProviders.push(provider);
}
});
const filterChips = [{ id: 'all', name: 'All Providers' }];
// Add individual addon chips
addonProviders
.sort((a, b) => {
const indexA = installedAddons.findIndex(addon => addon.id === a);
const indexB = installedAddons.findIndex(addon => addon.id === b);
return indexA - indexB;
})
.forEach(provider => {
const installedAddon = installedAddons.find(addon => addon.id === provider);
filterChips.push({ id: provider, name: installedAddon?.name || provider });
});
// Add single grouped plugins chip if there are any plugins
if (pluginProviders.length > 0) {
filterChips.push({ id: 'grouped-plugins', name: 'Plugins' });
}
return filterChips;
}
// Normal mode - individual chips for all providers
return [ return [
{ id: 'all', name: 'All Providers' }, { id: 'all', name: 'All Providers' },
...Array.from(allProviders) ...Array.from(allProviders)
@ -889,19 +933,26 @@ export const StreamsScreen = () => {
return { id: provider, name: displayName }; return { id: provider, name: displayName };
}) })
]; ];
}, [availableProviders, type, episodeStreams, groupedStreams]); }, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
const sections = useMemo(() => { const sections = useMemo(() => {
const streams = type === 'series' ? episodeStreams : groupedStreams; const streams = type === 'series' ? episodeStreams : groupedStreams;
const installedAddons = stremioService.getInstalledAddons(); const installedAddons = stremioService.getInstalledAddons();
// Filter streams by selected provider - only if not "all" // Filter streams by selected provider
const filteredEntries = Object.entries(streams) const filteredEntries = Object.entries(streams)
.filter(([addonId]) => { .filter(([addonId]) => {
// If "all" is selected, show all providers // If "all" is selected, show all providers
if (selectedProvider === 'all') { if (selectedProvider === 'all') {
return true; return true;
} }
// In grouped mode, handle special 'grouped-plugins' filter
if (settings.streamDisplayMode === 'grouped' && selectedProvider === 'grouped-plugins') {
const isInstalledAddon = installedAddons.some(addon => addon.id === addonId);
return !isInstalledAddon; // Show only plugins (non-installed addons)
}
// Otherwise only show the selected provider // Otherwise only show the selected provider
return addonId === selectedProvider; return addonId === selectedProvider;
}) })
@ -914,17 +965,59 @@ export const StreamsScreen = () => {
if (indexA !== -1) return -1; if (indexA !== -1) return -1;
if (indexB !== -1) return 1; if (indexB !== -1) return 1;
return 0; return 0;
}) });
.map(([addonId, { addonName, streams: providerStreams }]) => {
// Check if we should group all streams under one section
if (settings.streamDisplayMode === 'grouped') {
// Separate streams by type: installed addons vs plugins
const addonStreams: Stream[] = [];
const pluginStreams: Stream[] = [];
const addonNames: string[] = [];
const pluginNames: string[] = [];
filteredEntries.forEach(([addonId, { addonName, streams: providerStreams }]) => {
const isInstalledAddon = installedAddons.some(addon => addon.id === addonId);
if (isInstalledAddon) {
addonStreams.push(...providerStreams);
if (!addonNames.includes(addonName)) {
addonNames.push(addonName);
}
} else {
pluginStreams.push(...providerStreams);
if (!pluginNames.includes(addonName)) {
pluginNames.push(addonName);
}
}
});
const sections = [];
if (addonStreams.length > 0) {
sections.push({
title: addonNames.join(', '),
addonId: 'grouped-addons',
data: addonStreams
});
}
if (pluginStreams.length > 0) {
sections.push({
title: pluginNames.join(', '),
addonId: 'grouped-plugins',
data: pluginStreams
});
}
return sections;
} else {
// Use separate sections for each provider (current behavior)
return filteredEntries.map(([addonId, { addonName, streams: providerStreams }]) => {
return { return {
title: addonName, title: addonName,
addonId, addonId,
data: providerStreams data: providerStreams
}; };
}); });
}
return filteredEntries; }, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
}, [selectedProvider, type, episodeStreams, groupedStreams]);
const episodeImage = useMemo(() => { const episodeImage = useMemo(() => {
if (episodeThumbnail) { if (episodeThumbnail) {
@ -993,11 +1086,7 @@ export const StreamsScreen = () => {
const isProviderLoading = loadingProviders[section.addonId]; const isProviderLoading = loadingProviders[section.addonId];
return ( return (
<Animated.View <View style={styles.sectionHeaderContainer}>
entering={FadeIn.duration(400)}
exiting={FadeOut.duration(150)}
style={styles.sectionHeaderContainer}
>
<View style={styles.sectionHeaderContent}> <View style={styles.sectionHeaderContent}>
<Text style={styles.streamGroupTitle}>{section.title}</Text> <Text style={styles.streamGroupTitle}>{section.title}</Text>
{isProviderLoading && ( {isProviderLoading && (
@ -1009,7 +1098,7 @@ export const StreamsScreen = () => {
</View> </View>
)} )}
</View> </View>
</Animated.View> </View>
); );
}, [styles.streamGroupTitle, styles.sectionHeaderContainer, styles.sectionHeaderContent, styles.sectionLoadingIndicator, styles.sectionLoadingText, loadingProviders, colors.primary]); }, [styles.streamGroupTitle, styles.sectionHeaderContainer, styles.sectionHeaderContent, styles.sectionLoadingIndicator, styles.sectionLoadingText, loadingProviders, colors.primary]);