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
scraperTimeout: number; // Timeout for scraper execution in seconds
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 = {
@ -64,6 +65,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
enableLocalScrapers: true,
scraperTimeout: 60, // 60 seconds timeout
enableScraperUrlValidation: true, // Enable URL validation by default
streamDisplayMode: 'separate', // Default to separate display by provider
};
const SETTINGS_STORAGE_KEY = 'app_settings';

View file

@ -739,6 +739,22 @@ const PluginsScreen: React.FC = () => {
disabled={!settings.enableLocalScrapers}
/>
</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 file

@ -101,15 +101,8 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
};
}, [stream.name, stream.title, stream.behaviorHints, stream.size]);
// Animation delay based on index - stagger effect
const enterDelay = 100 + (index * 30);
return (
<Animated.View
entering={FadeInDown.duration(200).delay(enterDelay)}
exiting={FadeOut.duration(150)}
>
<TouchableOpacity
<TouchableOpacity
style={[
styles.streamCard,
isLoading && styles.streamCardLoading
@ -169,7 +162,6 @@ const StreamCard = memo(({ stream, onPress, index, isLoading, statusMessage, the
/>
</View>
</TouchableOpacity>
</Animated.View>
);
});
@ -232,11 +224,7 @@ const ProviderFilter = memo(({
const styles = React.useMemo(() => createStyles(theme.colors), [theme.colors]);
const renderItem = useCallback(({ item, index }: { item: { id: string; name: string }; index: number }) => (
<Animated.View
entering={FadeIn.duration(300).delay(100 + index * 40)}
exiting={FadeOut.duration(150)}
>
<TouchableOpacity
<TouchableOpacity
key={item.id}
style={[
styles.filterChip,
@ -251,13 +239,10 @@ const ProviderFilter = memo(({
{item.name}
</Text>
</TouchableOpacity>
</Animated.View>
), [selectedProvider, onSelect, styles]);
return (
<Animated.View
entering={FadeIn.duration(300)}
>
<View>
<FlatList
data={providers}
renderItem={renderItem}
@ -277,7 +262,7 @@ const ProviderFilter = memo(({
index,
})}
/>
</Animated.View>
</View>
);
});
@ -385,7 +370,12 @@ export const StreamsScreen = () => {
if (providersWithStreams.length > 0) {
logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`);
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
@ -407,11 +397,28 @@ export const StreamsScreen = () => {
}, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type]);
// 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(() => {
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');
}
}, [selectedProvider, availableProviders]);
}, [selectedProvider, availableProviders, episodeStreams, groupedStreams, type]);
// Update useEffect to check for sources
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 [
{ id: 'all', name: 'All Providers' },
...Array.from(allProviders)
@ -889,19 +933,26 @@ export const StreamsScreen = () => {
return { id: provider, name: displayName };
})
];
}, [availableProviders, type, episodeStreams, groupedStreams]);
}, [availableProviders, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
const sections = useMemo(() => {
const streams = type === 'series' ? episodeStreams : groupedStreams;
const installedAddons = stremioService.getInstalledAddons();
// Filter streams by selected provider - only if not "all"
// Filter streams by selected provider
const filteredEntries = Object.entries(streams)
.filter(([addonId]) => {
// If "all" is selected, show all providers
if (selectedProvider === 'all') {
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
return addonId === selectedProvider;
})
@ -914,17 +965,59 @@ export const StreamsScreen = () => {
if (indexA !== -1) return -1;
if (indexB !== -1) return 1;
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 {
title: addonName,
addonId,
data: providerStreams
};
});
return filteredEntries;
}, [selectedProvider, type, episodeStreams, groupedStreams]);
}
}, [selectedProvider, type, episodeStreams, groupedStreams, settings.streamDisplayMode]);
const episodeImage = useMemo(() => {
if (episodeThumbnail) {
@ -993,11 +1086,7 @@ export const StreamsScreen = () => {
const isProviderLoading = loadingProviders[section.addonId];
return (
<Animated.View
entering={FadeIn.duration(400)}
exiting={FadeOut.duration(150)}
style={styles.sectionHeaderContainer}
>
<View style={styles.sectionHeaderContainer}>
<View style={styles.sectionHeaderContent}>
<Text style={styles.streamGroupTitle}>{section.title}</Text>
{isProviderLoading && (
@ -1009,7 +1098,7 @@ export const StreamsScreen = () => {
</View>
)}
</View>
</Animated.View>
</View>
);
}, [styles.streamGroupTitle, styles.sectionHeaderContainer, styles.sectionHeaderContent, styles.sectionLoadingIndicator, styles.sectionLoadingText, loadingProviders, colors.primary]);